Полезен ли параллелизм с блокировкой для транзакций записи данных

Поможет ли параллелизм повысить производительность заблокированного объекта, должен ли он работать в однопоточном режиме или есть другой метод?

Я заметил, что при доступе к набору данных и добавлении строк из нескольких потоков были выброшены исключения. Поэтому я создал «поточно-ориентированную» версию для добавления строк путем блокировки таблицы перед обновлением строки. Эта реализация работает, но при большом количестве транзакций она кажется медленной.

public partial class HaMmeRffl
{
    public partial class PlayerStatsDataTable
    {
        public void AddPlayerStatsRow(int PlayerID, int Year, int StatEnum, int Value, DateTime Timestamp)
        {
            lock (TeamMemberData.Dataset.PlayerStats)
            {
                HaMmeRffl.PlayerStatsRow testrow = TeamMemberData.Dataset.PlayerStats.FindByPlayerIDYearStatEnum(PlayerID, Year, StatEnum);
                if (testrow == null)
                {
                    HaMmeRffl.PlayerStatsRow newRow = TeamMemberData.Dataset.PlayerStats.NewPlayerStatsRow();
                    newRow.PlayerID = PlayerID;
                    newRow.Year = Year;
                    newRow.StatEnum = StatEnum;
                    newRow.Value = Value;
                    newRow.Timestamp = Timestamp;
                    TeamMemberData.Dataset.PlayerStats.AddPlayerStatsRow(newRow);
                }
                else
                {
                    testrow.Value = Value;
                    testrow.Timestamp = Timestamp;
                }

            }
        }
    }
}

Теперь я могу безопасно вызывать это из нескольких потоков, но действительно ли это мне что-нибудь дает? Могу ли я сделать это по-другому для повышения производительности. Например, есть ли способ использовать пространство имен System.Collections.Concurrent для оптимизации производительности или какие-либо другие методы?

Кроме того, я обновляю базовую базу данных после обновления всего набора данных, а это занимает очень много времени. Будет ли это считаться операцией ввода-вывода и стоит ли использовать параллельную обработку, обновляя ее после обновления каждой строки в наборе данных (или некоторого количества строк).

ОБНОВЛЕНИЕ

Я написал код для тестирования одновременной и последовательной обработки, который показывает, что параллельная обработка занимает примерно 30% больше времени, и здесь я должен использовать последовательную обработку. Я предполагаю, что это связано с тем, что из-за блокировки базы данных накладные расходы на ConcurrentQueue становятся более дорогостоящими, чем выгоды от параллельной обработки. Правильно ли это заключение и могу ли я что-нибудь сделать, чтобы ускорить обработку? или я застрял в отношении Datatable «Вы должны синхронизировать любые операции записи».

Вот мой тестовый код, который может быть неверным с научной точки зрения. Вот таймер и звонки между ними.

            dbTimer.Restart();
            Queue<HaMmeRffl.PlayersRow.PlayerValue> addPlayerRow = InsertToPlayerQ(addUpdatePlayers);
            Queue<HaMmeRffl.PlayerStatsRow.PlayerStatValue> addPlayerStatRow                     = InsertToPlayerStatQ(addUpdatePlayers);
            UpdatePlayerStatsInDB(addPlayerRow, addPlayerStatRow);
            dbTimer.Stop();
            System.Diagnostics.Debug.Print("Writing to the dataset took {0} seconds single threaded", dbTimer.Elapsed.TotalSeconds);

            dbTimer.Restart();
            ConcurrentQueue<HaMmeRffl.PlayersRow.PlayerValue> addPlayerRows                    = InsertToPlayerQueue(addUpdatePlayers);
            ConcurrentQueue<HaMmeRffl.PlayerStatsRow.PlayerStatValue> addPlayerStatRows                    = InsertToPlayerStatQueue(addUpdatePlayers);
            UpdatePlayerStatsInDB(addPlayerRows, addPlayerStatRows);
            dbTimer.Stop();
            System.Diagnostics.Debug.Print("Writing to the dataset took {0} seconds concurrently", dbTimer.Elapsed.TotalSeconds);

В обоих примерах я одинаково добавляю к Queue и ConcurrentQueue однопоточные. Единственное отличие - это вставка в datatable. Однопоточный подход включает следующие вставки:

    private static void UpdatePlayerStatsInDB(Queue<HaMmeRffl.PlayersRow.PlayerValue> addPlayerRows, Queue<HaMmeRffl.PlayerStatsRow.PlayerStatValue> addPlayerStatRows)
    {
        try
        {
            HaMmeRffl.PlayersRow.PlayerValue row;
            while (addPlayerRows.Count > 0)
            {
                row = addPlayerRows.Dequeue();
                TeamMemberData.Dataset.Players.AddPlayersRow(
                    row.PlayerID, row.Name, row.PosEnum, row.DepthEnum,
                    row.TeamID, row.RosterTimestamp, row.DepthTimestamp,
                    row.Active, row.NewsUpdate);
            }
        }
        catch (Exception)
        {
            TeamMemberData.Dataset.Players.RejectChanges();
        }

        try
        {
            HaMmeRffl.PlayerStatsRow.PlayerStatValue row;
            while (addPlayerStatRows.Count > 0)
            {
                row = addPlayerStatRows.Dequeue();
                TeamMemberData.Dataset.PlayerStats.AddUpdatePlayerStatsRow(
                    row.PlayerID, row.Year, row.StatEnum, row.Value, row.Timestamp);
            }
        }
        catch (Exception)
        {
            TeamMemberData.Dataset.PlayerStats.RejectChanges();
        }

        TeamMemberData.Dataset.Players.AcceptChanges();
        TeamMemberData.Dataset.PlayerStats.AcceptChanges();

    }

Конкурент добавляет следующее

    private static void UpdatePlayerStatsInDB(ConcurrentQueue<HaMmeRffl.PlayersRow.PlayerValue> addPlayerRows, ConcurrentQueue<HaMmeRffl.PlayerStatsRow.PlayerStatValue> addPlayerStatRows)
    {
        Action actionPlayer = () =>
        {
            HaMmeRffl.PlayersRow.PlayerValue row;
            while (addPlayerRows.TryDequeue(out row))
            {
                TeamMemberData.Dataset.Players.AddPlayersRow(
                    row.PlayerID, row.Name, row.PosEnum, row.DepthEnum,
                    row.TeamID, row.RosterTimestamp, row.DepthTimestamp,
                    row.Active, row.NewsUpdate);
            }
        };

        Action actionPlayerStat = () =>
        {
            HaMmeRffl.PlayerStatsRow.PlayerStatValue row;
            while (addPlayerStatRows.TryDequeue(out row))
            {
                TeamMemberData.Dataset.PlayerStats.AddUpdatePlayerStatsRow(
                    row.PlayerID, row.Year, row.StatEnum, row.Value, row.Timestamp);
            }
        };

        Action[] actions = new Action[Environment.ProcessorCount * 2];
        for (int i = 0; i < Environment.ProcessorCount; i++)
        {
            actions[i * 2] = actionPlayer;
            actions[i * 2 + 1] = actionPlayerStat;
        }

        try
        {
            // Start ProcessorCount concurrent consuming actions.
            Parallel.Invoke(actions);
        }
        catch (Exception)
        {
            TeamMemberData.Dataset.Players.RejectChanges();
            TeamMemberData.Dataset.PlayerStats.RejectChanges();
        }

        TeamMemberData.Dataset.Players.AcceptChanges();
        TeamMemberData.Dataset.PlayerStats.AcceptChanges();

    }

Разница во времени составляет 4,6 секунды для однопоточного и 6,1 для параллельного вызова.


person Harrison    schedule 27.09.2013    source источник
comment
Возможно, вы захотите заглянуть в какую-то систему очереди сообщений и иметь один поток, обрабатывающий все действия базы данных / таблицы данных, в то время как остальная часть программы может продолжить работу после того, как она поместит свое действие в очередь.   -  person Scott Chamberlain    schedule 27.09.2013
comment
@ScottChamberlain Вот на что я сейчас смотрю. Я собирался с ConcurrentQueue и собирался попытаться использовать несколько потоков, чтобы попытаться ускорить транзакции базы данных, но потом мне было интересно, следует ли мне просто использовать обычную очередь и один поток   -  person Harrison    schedule 27.09.2013
comment
Полная реализация зависит от того, как вы выполняете весь свой проект (например, есть ли другие функции, кроме AddPlayerStatsRow). Самый простой способ сделать это - BlockingCollection, поддерживаемый ConcurrentQueue запущенным в своем собственном потоке (вы просто вызываете GetConsumingEnumerable() в цикле foreach и позволяете ему работать вечно). Более сложные решения могут включать написание собственного расширения для _4 _.   -  person Scott Chamberlain    schedule 27.09.2013


Ответы (2)


Блокировка и транзакции не подходят для параллелизма и производительности.

1) Попробуйте избежать блокировки : Нужно ли будет разным потокам обновлять одну и ту же строку в наборе данных?

2) минимизировать время блокировки.

Для работы с базой данных можно попробовать пакетное обновление будущего ADO.NET: http://msdn.microsoft.com/en-us/library/ms810297.aspx.

person Whitesmell    schedule 27.09.2013
comment
К сожалению, когда я делал это без блокировки, я получал исключения. Иногда я обновляю одну и ту же строку в наборе данных с разными потоками. Я не понимаю, как мне избежать блокировки связанной таблицы. Я действительно считаю, что у меня самая маленькая блокировка, поскольку это в основном оболочка для метода Addrow по умолчанию. Видите ли вы возможности для улучшения моего кода? - person Harrison; 27.09.2013

Многопоточность может помочь до некоторой степени, потому что, как только данные пройдут через границу вашего приложения, вы начнете ждать ввода-вывода, здесь вы можете выполнять асинхронную обработку, потому что ваше приложение не контролирует различные параметры (доступ к ресурсам, скорость сети и т. Д.), Это обеспечит лучший пользовательский интерфейс (если приложение пользовательского интерфейса).

Теперь для вашего сценария вы можете использовать какую-то очередь производителя / потребителя, как только строка становится доступной в очереди, другой поток начинает ее обрабатывать, но снова это будет работать до определенной степени.

person TalentTuner    schedule 01.10.2013