Почему Interlocked.Increment дает неверный результат в цикле Parallel.ForEach?

У меня есть задание на перенос, и я должен проверить достоверность данных. Чтобы уведомить администратора об успешности/неудаче валидации, я использую счетчик, чтобы сравнить количество строк из таблицы 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.

Ответ 1

Interlocked.Increment свойство всегда будет разбито. Фактически компилятор VB перезаписывает его как:

Value = <value from Property>
Interlocked.Increment(Value)
<Property> = Value

Таким образом, устраняются любые гарантии на резьбу, предоставляемые Increment. Измените его как поле. VB переписывает любое свойство, переданное как параметр ByRef, в код, похожий на приведенный выше.