无法通过并发线程访问来破坏数据

时间:2012-11-26 07:29:10

标签: c# .net multithreading locking

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

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

以下代码段创建了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 ++。该值始终产生预期值。

3 个答案:

答案 0 :(得分:4)

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

目前,您的线程将大部分时间花在浩大的上,或者写入控制台。这大大减少了碰撞的可能性。

此外,我注意到缺少证据并不是缺席的证据 - 并发问题确实很难引发,特别是如果您碰巧在具有强内存模型和内部原子递增/递减的CPU上运行JIT可以使用的说明。可能是您永远不会在您的特定计算机上引发问题 - 但是同一程序可能会在另一台计算机上失败。

答案 1 :(得分:1)

除了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();

    }

它不是;)

结果:我比j低约15%(百分比!)。在我的机器上。拥有一个八线程机器,probabyl甚至可能使结果更加迫在眉睫,因为如果几个任务真正并行运行并且不仅仅是先发制人,那么错误更有可能发生。

上述代码当然有缺陷:(
如果任务被抢占,就在i ++之后,所有其他任务继续增加i和j,所以我希望与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正是应该的!

答案 2 :(得分:1)

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);
}