Как PLINQ существующий запрос LINQ с объединениями? - PullRequest
13 голосов
/ 28 сентября 2011

Я использую LINQ для сравнения двух DataSets друг с другом, чтобы создать новые строки и обновить существующие. Я заметил, что полное сравнение длится ~ 1,5 часа, и только одно из двух ядер занято (Task-Manager использует процессор на 50-52%). Я должен признать, что я совершенно новичок в параллельном LINQ, но я предполагаю, что это может значительно повысить производительность.

Итак, мой вопрос: как и что я должен распараллеливать?

Это исходные запросы (сведены к основам):

'check for new data
Dim srcUnique = From row In src.Email_Total
                Select Ticket_ID = row.ticket_id, Interaction = row.interaction, ModifiedAt = row.modified_time

Dim destUnique = From row In dest.ContactDetail
                 Where row.ContactRow.fiContactType = emailContactType.idContactType
                 Select row.ContactRow.Ticket_ID, row.Interaction, row.ModifiedAt

'get all emails(contactdetails) that are in source but not in destination
Dim diffRows = srcUnique.Except(destUnique).ToList

'get all new emails(according to ticket_id) for calculating contact columns
Dim newRowsTickets = (From row In src.Email_Total
                     Join d In diffRows
                     On row.ticket_id Equals d.Ticket_ID _
                     And row.interaction Equals d.Interaction _
                     And row.modified_time Equals d.ModifiedAt
                     Group row By Ticket_ID = row.ticket_id Into NewTicketRows = Group).ToList

For Each ticket In newRowsTickets
     Dim contact = dest.Contact.FindByTicket_IDfiContactType(ticket.Ticket_ID, emailContactType.idContactType)
     If contact Is Nothing Then
          ' Create new Contact with many sub-queries on this ticket(omitted) ****'
          Dim newContact = Me.dest.Contact.NewContactRow
          dest.Contact.AddContactRow(newContact)
          contact = newContact
     Else
          ' Update Contact with many sub-queries on this ticket(omitted) '
     End If
     daContact.Update(dest.Contact)

     ' Add new ContactDetail-Rows from this Ticket(this is the counterpart of the src.Email_Total-Rows, details omitted) '
     For Each newRow In ticket.NewTicketRows
         Dim newContactDetail = dest.ContactDetail.NewContactDetailRow
         newContactDetail.ContactRow = contact
         dest.ContactDetail.AddContactDetailRow(newContactDetail)
     Next
     daContactDetails.Update(dest.ContactDetail)
Next

Примечание : daContact и daContactDetails равны SqlDataAdapters, source и dest равны DataSets, Contact и ContactDetail равны DataTables, где каждый ContactDetail принадлежит контакту.

Даже если бы не оба ядра использовали 100% ЦП, я предполагаю, что это значительно увеличило бы производительность, если бы я распараллеливал запросы, потому что второе ядро ​​почти бездействует. for each также может быть хорошим местом для оптимизации, поскольку билеты не связаны друг с другом. Поэтому я предполагаю, что я мог бы зацикливаться на нескольких потоках и создавать / обновлять записи параллельно. Но как это сделать с PLINQ?

Примечание : Как я уже упоминал в комментариях, производительность для меня пока не является ключевым фактором, поскольку единственной целью сервера является синхронизация базы данных MySQL (на другом сервере). ) с MS SQL-сервером (на том же сервере, что и эта Windows-служба). Он выступает в качестве источника отчетов, созданных другой службой. Но эти отчеты создаются только один раз в день. Но кроме этого мне было интересно изучать PLINQ, потому что я думал, что это может быть отличным упражнением. Это занимает 1,5 часа, только если целевая БД пуста и все записи должны быть созданы. Если обе базы данных почти синхронизированы, этот метод занимает всего ~ 1 минуту. В будущем производительность станет более важной, поскольку электронная почта - это только один из нескольких типов контактов (чат + звонки будут превышать 1 млн. Записей). Я думаю, что в любом случае мне понадобится какая-то (LINQ) передача данных.

Если что-то неясно, я обновлю свой ответ соответственно. Заранее спасибо.


Редактировать : Вот результат моих исследований и попыток:

Вопрос: Как "PLINQ" существующий запрос LINQ с объединениями?

Ответ : Обратите внимание, что некоторые операторы LINQ являются двоичными - они принимают два IEnumerable в качестве входных данных. Join является прекрасным примером такого оператора. В этих случаях тип самого левого источника данных определяет, будет ли использоваться LINQ или PLINQ. Таким образом, вам нужно только вызвать AsParallel в первом источнике данных, чтобы запрос выполнялся параллельно:

IEnumerable<T> leftData = ..., rightData = ...;
var q = from x in leftData.AsParallel()
        join y in rightData on x.a == y.b
        select f(x, y);

Но если я изменю свой запрос следующим образом (обратите внимание на AsParallel):

Dim newRowsTickets = (From row In src.Email_Total.AsParallel()
                                        Join d In diffRows
                                        On row.ticket_id Equals d.Ticket_ID _
                                        And row.interaction Equals d.Interaction _
                                        And row.modified_time Equals d.ModifiedAt
                                    Group row By Ticket_ID = row.ticket_id Into NewTicketRows = Group).ToList

Компилятор будет жаловаться, что мне нужно также добавить AsParallel к нужному источнику данных. Так что, похоже, это проблема VB.NET или отсутствие документации (статья за 2007 год). Я предполагаю последнее, потому что (кроме этой рекомендуемой) статьи также говорится, что вам нужно добавить System.Concurrency.dll вручную, но на самом деле это часть .NET 4.0 Framework и пространства имен Sytem.Threading.Tasks.

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

Поэтому я решил распараллелить for-each, что так же просто, как с LINQ-Queries, вам просто нужно добавить AsParallel() в конце. Но я понял, что мне нужно форсировать параллелизм с WithExecutionMode(ParallelExecutionMode.ForceParallelism), в противном случае .NET решит использовать только одно ядро ​​для этого цикла. Я также хотел сказать .NET, что я хочу использовать как можно больше потоков, но не более 8: WithDegreeOfParallelism(8).

Теперь оба ядра работают одновременно, но загрузка процессора остается на 54%.

Итак, это пока версия PLINQ:

Dim diffRows = srcUnique.AsParallel.Except(destUnique.AsParallel).ToList

Dim newRowsTickets = (From row In src.Email_Total.AsParallel()
                        Join d In diffRows.AsParallel()
                        On row.ticket_id Equals d.Ticket_ID _
                        And row.interaction Equals d.Interaction _
                        And row.modified_time Equals d.ModifiedAt
                    Group row By Ticket_ID = row.ticket_id Into NewTicketRows = Group).ToList

For Each ticket In newRowsTickets.
                    AsParallel().
                      WithDegreeOfParallelism(8).
                       WithExecutionMode(ParallelExecutionMode.ForceParallelism)
    '  blah,blah ...  '

    'add new ContactDetails for this Ticket(only new rows)
    For Each newRow In ticket.NewTicketRows.
                                AsParallel().
                                    WithExecutionMode(ParallelExecutionMode.Default)
        ' blah,blah ... '
    Next
    daContactDetails.Update(dest.ContactDetail)
Next

К сожалению, я не вижу никаких преимуществ в производительности от использования AsParallel по сравнению с последовательным режимом:

for each с AsParallel (чч: мм: сс.мм):

09/29/2011 18:54:36: Contacts/ContactDetails created or modified. Duration: 01:21:34.40

и без:

09/29/2011 16:02:55: Contacts/ContactDetails created or modified. Duration: 01:21:24.50

Может кто-нибудь объяснить мне этот результат? Отвечает ли база данных за доступ к записи в for each за такое же время?


Ниже приведены рекомендуемые значения:

1 Ответ

1 голос
/ 07 октября 2011

Есть 3 пункта, которые стоит изучить дальше,

  1. Не используйте .toList (). Я могу ошибаться, но я думаю, используя .ToList этот способ не позволит компилятору оптимизировать запрос, если возможна дальнейшая оптимизация.
  2. Используйте свою собственную операцию фильтрации для сравнения данных из обоих destionations. Это может дать вам лучшую производительность.
  3. Посмотрите, можете ли вы использовать LinqDataview , чтобы обеспечить лучшее производительность.

    Не думаю, что вы получите преимущество от PLinq при выполнении вставки. Посмотрите этот ответ для более подробной информации.

Надеюсь, это поможет. Пожалуйста, спросите, нуждаетесь ли вы в разъяснениях по любому из вышеуказанных пунктов.

...