У меня есть задание на перенос, и я должен проверить достоверность данных. Чтобы уведомить администратора об успешности/неудаче валидации, я использую счетчик, чтобы сравнить количество строк из таблицы Foo в Database1 с количеством строк из таблицы Foo в Database2.
Каждая строка из базы данных2 проверяется против соответствующей строки в Database1. Чтобы ускорить процесс, я использую цикл Parallel.ForEach
.
Моя первоначальная проблема заключалась в том, что счет всегда отличался от того, что я ожидал. Позже я обнаружил, что операции +=
и -=
не являются потокобезопасными (не атомарными). Чтобы исправить проблему, я обновил код, чтобы использовать Interlocked.Increment
в переменной счетчика. Этот код печатает подсчет, который ближе к фактическому счету, но, тем не менее, он кажется различным для каждого исполнения и не дает ожидаемого результата:
Private countObjects As Integer
Private Sub MyMainFunction()
Dim objects As List(Of MyObject)
'Query with Dapper, unrelevant to the problem.
Using connection As New System.Data.SqlClient.SqlConnection("aConnectionString")
objects = connection.Query("SELECT * FROM Foo") 'Returns around 81000 rows.
End Using
Parallel.ForEach(objects, Sub(u) MyParallelFunction(u))
Console.WriteLine(String.Format("Count : {0}", countObjects)) 'Prints "Count : 80035" or another incorrect count, which seems to differ on each execution of MyMainFunction.
End Sub
Private Sub MyParallelFunction(obj As MyObject)
Interlocked.Increment(countObjects) 'Breakpoint Hit Count is at around 81300 or another incorrect number when done.
'Continues executing unrelated code using obj...
End Sub
После некоторых экспериментов с другими способами создания потокового потока с добавлением, я обнаружил, что обертывание приращения в SyncLock
на фиктивном ссылочном объекте дает ожидаемый результат:
Private countObjects As Integer
Private locker As SomeType
Private Sub MyMainFunction()
locker = New SomeType()
Dim objects As List(Of MyObject)
'Query with Dapper, unrelevant to the problem.
Using connection As New System.Data.SqlClient.SqlConnection("aConnectionString")
objects = connection.Query("SELECT * FROM Foo") 'Returns around 81000 rows.
End Using
Parallel.ForEach(objects, Sub(u) MyParallelFunction(u))
Console.WriteLine(String.Format("Count : {0}", countObjects)) 'Prints "Count : 81000".
End Sub
Private Sub MyParallelFunction(obj As MyObject)
SyncLock locker
countObjects += 1 'Breakpoint Hit Count is 81000 when done.
End SyncLock
'Continues executing unrelated code using obj...
End Sub
Почему первый фрагмент кода не работает должным образом? Самое запутанное - это то, что точка попадания точки останова дает неожиданные результаты.
Является ли мое понимание Interlocked.Increment
или атомных операций ошибочным? Я бы предпочел не использовать SyncLock
на фиктивном объекте, и я надеюсь, что есть способ сделать это чисто.
Update:
- Я запустил пример в
Debug
наAny CPU
. - Я использую
ThreadPool.SetMaxThreads(60, 60)
upper в стеке, потому что в какой-то момент я запрашиваю базу данных Access. Может ли это вызвать проблему? - Может ли вызов
Increment
запутаться с цикломParallel.ForEach
, заставляя его выйти до выполнения всех задач?
Обновление 2 (Методология):
- Мои тесты выполняются с максимально приближенным кодом к тому, что отображается здесь, за исключением типов объектов и строки запроса.
- Запрос всегда дает одинаковое количество результатов, и я всегда проверяю
objects.Count
на точке останова, прежде чем продолжить доParallel.ForEach
. - Единственный код, который изменяется между исполнением, заменяется на
Interlocked.Increment
наSyncLock locker
иcountObjects += 1
.
Обновление 3
Я создал SSCCE, скопировав свой код в новом консольном приложении и заменив внешние классы и код.
Это метод Main
консольного приложения:
Sub Main()
Dim oClass1 As New Class1
oClass1.MyMainFunction()
End Sub
Это определение Class1
:
Imports System.Threading
Public Class Class1
Public Class Dummy
Public Sub New()
End Sub
End Class
Public Class MyObject
Public Property Id As Integer
Public Sub New(p_Id As Integer)
Id = p_Id
End Sub
End Class
Public Property countObjects As Integer
Private locker As Dummy
Public Sub MyMainFunction()
locker = New Dummy()
Dim objects As New List(Of MyObject)
For i As Integer = 1 To 81000
objects.Add(New MyObject(i))
Next
Parallel.ForEach(objects, Sub(u As MyObject)
MyParallelFunction(u)
End Sub)
Console.WriteLine(String.Format("Count : {0}", countObjects)) 'Interlock prints an incorrect count, different in each execution. SyncLock prints the correct count.
Console.ReadLine()
End Sub
'Interlocked
Private Sub MyParallelFunction(ByVal obj As MyObject)
Interlocked.Increment(countObjects)
End Sub
'SyncLock
'Private Sub MyParallelFunction(ByVal obj As MyObject)
' SyncLock locker
' countObjects += 1
' End SyncLock
'End Sub
End Class
Я все еще отмечаю то же поведение при переключении MyParallelFunction
от Interlocked.Increment
до SyncLock
.