Почему Interlocked.Increment дает неверный результат в цикле Parallel.ForEach?
У меня есть задача по миграции, и мне нужно проверить целевые данные, когда они будут выполнены. Чтобы уведомить администратора об успехе / неудаче проверок, я использую счетчик для сравнения количества строк в таблице Foo в базе данных Database1 с количеством строк в таблице Foo в базе данных Database2.
Каждая строка из Database2 проверяется на соответствие соответствующей строке в 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
Почему первый фрагмент кода не работает должным образом? Самая запутанная вещь - это Hit Hit Breakpoint, дающий неожиданные результаты.
Мое понимание Interlocked.Increment
или атомные операции несовершенны? Я бы предпочел не использовать SyncLock
на фиктивном объекте, и я надеюсь, что есть способ сделать это чисто.
Обновить:
- Я запускаю пример в
Debug
режим включенAny CPU
, - я использую
ThreadPool.SetMaxThreads(60, 60)
верхний в стеке, потому что я запрашиваю базу данных 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
,
1 ответ
Interlocked.Increment
на имущество всегда будет нарушено. Фактически, компилятор VB переписывает это как:
Value = <value from Property>
Interlocked.Increment(Value)
<Property> = Value
Таким образом нарушая любые гарантии потоков, предоставленные Increment
, Измените это, чтобы быть полем. VB перепишет любое свойство, переданное как ByRef
параметр для кода, который похож на выше.