【问题标题】:Cannot corrupt data with concurrent thread access不能通过并发线程访问破坏数据
【发布时间】:2012-11-26 17:20:38
【问题描述】:

我编写这个实验是为了向某人证明使用多个线程同时访问共享数据是一个很大的禁忌。令我惊讶的是,无论我创建了多少线程,我都无法创建并发问题,并且该值始终导致平衡值 0。我知道增量运算符不是线程安全的,这就是为什么有方法像Interlocked.Increment()Interlocked.Decrement()(这里也注明Is the ++ operator thread safe?)。

如果递增/递减运算符不是线程安全的,那么为什么下面的代码执行时没有任何问题并且结果达到预期值?

下面的 sn-p 创建了 2,000 个线程。 1,000 不断增加和 1,000 不断减少,以确保多个线程同时访问数据。更糟糕的是,在普通程序中,您不会拥有几乎那么多的线程。然而,尽管为了创建并发问题而夸大了数字,但该值始终导致平衡值为 0。

static void Main(string[] args)
    {
        Random random = new Random();
        int value = 0;

        for (int x=0; x<1000; x++)
        {
            Thread incThread = new Thread(() =>
            {
                for (int y=0; y<100; y++)
                {
                    Console.WriteLine("Incrementing");
                    value++;
                }
            });

            Thread decThread = new Thread(() =>
            {
                for (int z=0; z<100; z++)
                {
                    Console.WriteLine("Decrementing");
                    value--;
                }
            });

            incThread.Start();
            decThread.Start();
        }

        Thread.Sleep(TimeSpan.FromSeconds(15));
        Console.WriteLine(value);
        Console.ReadLine();
    }

我希望有人能给我一个解释,让我知道我在编写线程安全软件方面所做的所有努力都没有白费,或者这个实验在某些方面存在缺陷。我还尝试过所有线程递增并使用 ++i 而不是 i++。该值始终会产生预期值。

【问题讨论】:

  • 删除Console.WriteLineThread.Sleep试试
  • 你能把这个作为答案发布吗?很难将代码作为注释阅读。
  • 拥有 1000(或 20000)个线程不应该比拥有 4 个更容易发生损坏。

标签: c# .net multithreading locking


【解决方案1】:

如果您有两个线程在非常关闭时间递增和递减,您通常只会看到问题。 (还有内存模型问题,但它们是分开的。)这意味着您希望它们花费大部分时间递增和递减,以便为您提供操作冲突的最佳机会。

目前,您的线程将花费大量大部分时间休眠或写入控制台。这大大减少了碰撞的机会。

另外,我要指出的是,没有证据并不是没有证据 - 并发问题确实很难引发,特别是如果您碰巧在具有强大内存模型和内部原子增量/减量的 CPU 上运行JIT 可以使用的指令。可能您永远不会在您的特定机器上引发问题 - 但同一程序可能会在另一台机器上失败。

【讨论】:

  • 在移除周期性的console.writeline's, thread.sleeps,并将线程数增加到20,000后,它仍然不会导致任何问题。两条线相撞真的有那么难,而且情况如此夸张吗?这对于我的线程安全讲座来说肯定不是好兆头。
  • @Despertar: "将线程数增加到 20,000" 您是在循环中生成 20000 个线程还是让每个线程增加到 20000 个?不是很清楚。
  • @Jon Skeet 原来罪魁祸首是增量线程内的 Console.WriteLine() (我误解了您最初指的是哪个 Console.Writeline)。删除它会大大增加碰撞率并导致错误的值。谢谢。
【解决方案2】:

IMO 这些循环太短了。我敢打赌,当第二个线程启动时,第一个线程已经完成执行其循环并退出。尝试大幅增加每个线程执行的迭代次数。此时,您甚至可以只生成两个线程(移除外循环),这应该足以看到错误的值。

例如,使用以下代码在我的系统上得到完全错误的结果:

static void Main(string[] args)
{
    Random random = new Random();
    int value = 0;

    Thread incThread = new Thread(() =>
            {
                for (int y = 0; y < 2000000; y++)
                {
                    value++;
                }
            });

    Thread decThread = new Thread(() =>
            {
                for (int z = 0; z < 2000000; z++)
                {
                    value--;
                }
            });

    incThread.Start();
    decThread.Start();

    incThread.Join();
    decThread.Join();
    Console.WriteLine(value);
}

【讨论】:

    【解决方案3】:

    除了 Jon Skeets 回答:

    一个简单的测试,至少在我的小双核上很容易显示问题:

    Sub Main()
    
        Dim i As Long = 1
        Dim j As Long = 1
    
        Dim f = Sub()
                    While Interlocked.Read(j) < 10 * 1000 * 1000
                        i += 1
                        Interlocked.Increment(j)
                    End While
                End Sub
    
        Dim l As New List(Of Task)
        For n = 1 To 4
            l.Add(Task.Run(f))
        Next
    
        Task.WaitAll(l.ToArray)
        Console.WriteLine("i={0}   j={1}", i, j)
        Console.ReadLine()
    
    
    End Sub
    

    i 和 j 应该都具有相同的最终值。但他们没有!

    编辑

    如果你认为 C# 比 VB 更聪明:

        static void Main(string[] args)
        {
            long i = 1;
            long j = 1;
    
            Task[] t = new Task[4];
    
            for (int k = 0; k < 4; k++)
            {
                t[k] = Task.Run(() => {
                                while (Interlocked.Read(ref j) < (long)(10*1000*1000))
                                {
                                    i++;
                                    Interlocked.Increment(ref j);
                                }});
            }
    
            Task.WaitAll(t);
            Console.WriteLine("i = {0}   j = {1}", i, j);
            Console.ReadLine();
    
        }
    

    不是;)

    结果:i 比 j 低大约 15%(百分比!)。在我的机器上。拥有一台 8 线程机器,probabyl 甚至可能使结果更加迫在眉睫,因为如果多个任务真正并行运行并且不只是被抢占,那么错误更有可能发生。

    上面的代码当然有缺陷:(
    如果一个任务被抢占,就在 i++ 之后,所有其他任务继续增加 i 和 j,所以 i 应该不同于 j,即使“++”是原子的。不过有一个简单的解决方案:

        static void Main(string[] args)
        {
            long i = 0;
            int runs = 10*1000*1000;
    
            Task[] t = new Task[Environment.ProcessorCount];
    
            Stopwatch stp = Stopwatch.StartNew();
    
            for (int k = 0; k < t.Length; k++)
            {
                t[k] = Task.Run(() =>
                {
                    for (int j = 0; j < runs; j++ )
                    {
                        i++;
                    }
                });
            }
    
            Task.WaitAll(t);
    
            stp.Stop();
    
            Console.WriteLine("i = {0}   should be = {1}  ms={2}", i, runs * t.Length, stp.ElapsedMilliseconds);
            Console.ReadLine();
    
        }
    

    现在可以在循环语句中的某处抢占任务。但这不会影响i。因此,查看对i 影响的唯一方法是,如果一个任务在i++ 语句处被抢占。这就是要展示的内容:它可能会发生,并且当您运行的任务更少但运行时间更长时更有可能发生。
    如果你写 Interlocked.Increment(ref i); 而不是 i++ 代码运行时间更长(因为锁定),但 i 正是它应该的!

    【讨论】:

    • 这是一个很好的测试。不幸的是 i 和 j 对我来说都具有相同的值(它已经运行了 15 分钟)。同样使用没有 TaskCreationOptions.LongRunning 的任务将使用线程池,与普通线程相比,这应该会降低并发性。我的处理器是amd x4 620(未超频);这些天没有什么能接近顶峰。 (而且我不认为 C# 比 VB 更聪明,只是更优秀:)
    • @Despertar 15 分钟?在我的 i7-2600(8 线程)上不到一秒,在我的 Pentium B950(2 线程)上不到 5 秒。两者都显示了问题。你使用的是完全相同的代码吗?
    • 逐字复制我能得到和你一样的结果。感谢您的帮助,+1。
    猜你喜欢
    • 2011-07-21
    • 2018-11-20
    • 1970-01-01
    • 1970-01-01
    • 2017-12-03
    • 2011-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多