Почему 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 параметр для кода, который похож на выше.

Другие вопросы по тегам