为什么Interlocked.Increment在Parallel.ForEach循环中给出了错误的结果?

 a126128 发布于 2023-02-10 09:08

我有一个迁移工作,我需要在完成后验证目标数据.为了通知管理员验证成功/失败,我使用一个计数器来比较Database1中表Foo的行数和Database2中表Foo的行数.

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

为什么第一个代码段不能按预期工作?最令人困惑的是Breakpoint Hit Count会产生意想不到的结果.

我对Interlocked.Increment原子操作的理解有缺陷吗?我宁愿不使用SyncLock虚拟对象,我希望有一种方法可以干净利落地完成它.

更新:

我在Debug模式中运行示例Any CPU.

ThreadPool.SetMaxThreads(60, 60)在堆栈中使用upper,因为我在某个时候查询Access数据库.这会导致问题吗?

可以调用Increment乱码Parallel.ForEach循环,强制它在所有任务完成之前退出吗?

更新2(方法):

我的测试使用尽可能接近此处显示的代码执行,但对象类型和查询字符串除外.

查询总是给出相同数量的结果,并且我总是objects.Count在继续之前验证断点Parallel.ForEach.

在执行之间更改的唯一代码将Interlocked.Increment替换为SyncLock lockercountObjects += 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.IncrementSyncLock.

1 个回答
  • Interlocked.Increment在一个属性上将永远被打破.实际上,VB编译器将其重写为:

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

    从而打破了由...提供的任何线程保证Increment.将其更改为字段.VB将把作为ByRef参数传递的任何属性重写为类似于上面的代码.

    2023-02-10 09:13 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有