使用Parallel.Invoke和静态变量时出现奇怪的行为

3

我正在尝试测试C#的并行方法,以下是我的测试程序:

class Program
{
    static int counter;
    static void Main(string[] args)
    {
        counter = 0;
        Parallel.Invoke(
            () => func(1),
            () => func(2),
            () => func(3)
            );
        Console.Read();
    }


    static void func(int num)
    {
        for (int i = 0; i < 5;i++ )
        {
            Console.WriteLine(string.Format("This is function #{0} loop. counter - {1}", num, counter));
            counter++;
        }
    }
}

我尝试做的是有一个静态共享变量,每个函数实例都将其增加1。
我期望 counter 会按顺序打印 (1,2,3,...) 但输出结果令人惊讶:
This is function #1 loop. counter - 0
This is function #1 loop. counter - 1
This is function #1 loop. counter - 2
This is function #1 loop. counter - 3
This is function #1 loop. counter - 4
This is function #3 loop. counter - 5
This is function #2 loop. counter - 1
This is function #3 loop. counter - 6
This is function #3 loop. counter - 8
This is function #3 loop. counter - 9
This is function #3 loop. counter - 10
This is function #2 loop. counter - 7
This is function #2 loop. counter - 12
This is function #2 loop. counter - 13
This is function #2 loop. counter - 14

有人能解释一下这是为什么吗?

如果您想要按顺序递增计数器,使用Parallel.Invoke的意义何在?只需调用您的函数3次,而不需要任何并行处理即可。 - alex
10
这被称为“竞态条件”,当多个进程在并行线程上尝试访问和/或修改相同的变量实例时,会导致“意外”的结果。这不是奇怪的行为,而是你应该预料到的。 - John Willemse
3个回答

2
你的代码存在线程安全问题。例如,可能出现以下情况:
- 函数#2获取counter的值并在Console.WriteLine()中使用; - 函数#1获取counter的值,调用Console.WriteLine(),然后对counter进行递增操作; - 函数#1再次获取counter的值,再次调用Console.WriteLine(),再次递增counter的值; - 最终函数#2使用旧值调用Console.WriteLine()
此外,++本身不是线程安全的,因此最终结果可能不是15。
为了解决这两个问题,你可以使用Interlocked.Increment()函数。
for (int i = 0; i < 5; i++)
{
    int incrementedCounter = Interlocked.Increment(ref counter);
    Console.WriteLine("This is function #{0} loop. counter - {1}", num, incrementedCounter);
}

这样,您将得到增量之后的数字,而不是像原始代码中那样在之前。此外,这段代码仍然无法按正确顺序打印数字,但您可以确保每个数字都将被准确地打印一次。

如果您确实希望按正确顺序显示数字,则需要使用lock

private static readonly object lockObject = new object();

…

for (int i = 0; i < 5; i++)
{
    lock (lockObject)
    {
        Console.WriteLine("This is function #{0} loop. counter - {1}", num, counter);
        counter++;
    }
}

当然,如果您这样做,实际上并不会得到任何并行性,但我假设这不是您真正的代码。

谢谢!这解释了发生的事情。 - Stasel

1

实际上发生的事情是 - 调用只是将这些任务排队,运行时为这些任务分配线程,这给它带来了很多随机因素(哪个会首先被选中等)。

即使 msdn 文章也说明了这一点:

此方法可用于执行一组操作,可能并行执行。不保证操作执行的顺序或是否并行执行。无论完成是由于正常还是异常终止,此方法都不返回,直到提供的每个操作均已完成。


但这并不能解释为什么counter会以这种方式行事。 - svick

0

这个问题看起来像是许多线程访问同一个变量。这是并发的一个问题。 你可以尝试这样做:

    static object syncObj = new object();
    static void func(int num)
    {
        for (int i = 0; i < 5; i++)
        {
            lock (syncObj)
            {
                Console.WriteLine(string.Format("This is function #{0} loop. counter - {1}", num, counter));
                counter++;
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接