【问题标题】:Weird behavior when using Parallel.Invoke and static variable使用 Parallel.Invoke 和静态变量时的奇怪行为
【发布时间】:2013-05-01 11:37:54
【问题描述】:

我正在尝试测试 C# parallel 方法,这是我的测试程序:

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 个静态共享变量,每个函数实例将其增加 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 有什么意义?只需调用 func 3 次,无需任何并行。
  • 这称为竞争条件,并行线程上的多个进程尝试访问和/或修改同一个变量实例,从而导致“意外”结果。这不是奇怪的行为,而是您应该期待的。

标签: c# parallel-extensions


【解决方案1】:

问题是您的代码不是线程安全的。例如,可能发生的情况是这样的:

  • 函数#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++;
    }
}

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

【讨论】:

  • 谢谢!这准确地解释了发生的事情。
【解决方案2】:

实际上会发生什么 - Invoke 只是将这些任务排队,运行时为这些任务分配线程,这给它提供了很多随机元素(哪个将首先被拾取等)。

甚至 msdn 文章都这样说:

此方法可用于执行一组操作,可能是并行的。 不保证操作执行的顺序或它们是否并行执行。在提供的每个操作完成之前,此方法不会返回,无论完成是由于正常终止还是异常终止。

【讨论】:

  • 但这并不能解释为什么counter会这样。
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 2018-08-06
    • 1970-01-01
    • 2016-05-16
    • 2023-03-18
    • 2012-07-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多