2

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

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

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

4

3 回答 3

5

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

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

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

于 2012-11-26T07:43:48.407 回答
1

除了乔恩斯基茨回答:

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

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%(百分比!)。在我的机器上。拥有一台八线程机器,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正是它应该的!

于 2012-11-26T08:04:13.880 回答
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);
}
于 2012-11-26T08:53:42.907 回答