Полезен ли е паралелизмът със заключване за транзакции за запис на данни

Ще помогне ли паралелизмът за производителността на заключен обект, трябва ли да се изпълнява с една нишка или има друга техника?

Забелязах, че при достъп до набор от данни и добавяне на редове от множество нишки бяха хвърлени изключения. Затова създадох „безопасна за нишки“ версия за добавяне на редове чрез заключване на таблицата преди актуализиране на реда. Това внедряване работи, но изглежда бавно при много транзакции.

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 за оптимизиране на производителността или други методи?

Освен това актуализирам основната база данни, след като целият набор от данни е актуализиран и това отнема много време. Това ще се счита ли за I/O операция и дали си струва да използвате паралелна обработка, като я актуализирате, след като всеки ред се актуализира в набора от данни (или определен брой редове).

АКТУАЛИЗАЦИЯ

Написах някакъв код за тестване на едновременна срещу последователна обработка, който показва, че отнема около 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 по идентичен начин с една резба. Единствената разлика е вмъкването в таблицата с данни. Подходът с една нишка вмъква следното:

    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 за паралелния. Invoke.


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 цикъл и просто го оставяте да работи завинаги). По-сложните решения могат да включват писане на собствено разширение на SynchronizationContext.   -  person Scott Chamberlain    schedule 27.09.2013


Отговори (2)


Заключването и транзакциите не са добри за паралелизъм и производителност.

1) Опитайте да избегнете заключване: Ще трябва ли различни нишки да актуализират един и същ ред в набора от данни?

2) минимизиране на времето за заключване.

За използване на db операция може да опитате Batch Update future на ADO.NET: http://msdn.microsoft.com/en-us/library/ms810297.aspx

person Whitesmell    schedule 27.09.2013
comment
За съжаление, когато направих това без заключване, получавах изключения. Понякога актуализирам един и същ ред в набор от данни с различни нишки. Не виждам как мога да избегна заключването на свързаната таблица. Вярвам, че имам най-малката ключалка, която мога, тъй като тя е основно обвивка около метода Addrow по подразбиране, виждате ли място за подобрение в моя код? - person Harrison; 27.09.2013

Многонишковостта може да помогне до известна степен, защото след като данните преминат границата на вашето приложение, вие ще започнете да чакате I/O, тук можете да извършвате асинхронна обработка, тъй като приложението ви няма контрол върху различни параметри (достъп до ресурси, скорост на мрежата и т.н.), това ще даде по-добро потребителско изживяване (Ако UI приложение).

Сега за вашия сценарий, може да искате да използвате някакъв вид опашка производител/потребител, веднага щом ред е наличен в опашката, различна нишка започва да го обработва, но отново това ще работи до известна степен.

person TalentTuner    schedule 01.10.2013