【问题标题】:Task.WaitAll seems bugged [duplicate]Task.WaitAll 似乎有问题 [重复]
【发布时间】:2019-05-31 16:09:31
【问题描述】:

我正在开发一个执行各种数学运算的模拟工具。到目前为止,我还没有需要做并行操作,但现在我真的需要它们。在我读过的各种并行方法中,应该使用任务来实现最佳性能。

我写了这个简单的程序,但我意识到出了点问题。

private static int taskCounter { get; set; }
private static void SimpleTest() { taskCounter--; }

static void Main(string[] args)
{
    for (int N = 0; N < 100; N++)
    {
        taskCounter = 0;
        List<Task> taskList = new List<Task>();
        for (int i = 0; i < 100; i++)
        {
            taskCounter++;
            Task task = Task.Factory.StartNew(() => SimpleTest());
            taskList.Add(task);
        }
        Task.WaitAll(taskList.ToArray());
        Console.WriteLine("Unsolved: {0}", taskCounter);
    }
}

预期:未解决:所有迭代都为 0。 结果:

[...]
Unsolved: 0
Unsolved: 0
Unsolved: 2
Unsolved: -4
Unsolved: -1
Unsolved: -1
Unsolved: 0
Unsolved: 0
Unsolved: 2
Unsolved: -1
[...]

【问题讨论】:

  • taskCounter++taskCounter-- 不是原子操作。以并发方式运行这些操作是不安全的,并且会由于竞争条件而产生意外的任意结果...使用Interlocked.Increment / Interlocked.Decrement 方法进行原子递增和递减操作(互锁方法需要taskCounter一个字段;或保留taskCounter 一个属性,但显式实现一个支持字段,然后将由互锁方法修改...)
  • 要给出更礼貌的答案,您可能需要对货币进行更多研究才能真正了解发生了什么。使用访问相似数据的多个线程会变得非常复杂。但是,正如 elgonzo 所说,如果您让每个线程隔离,将结果存储到类似并发包(设计为线程安全的)中,然后在所有任务完成后合并结果,您可以避免很多复杂性。
  • 这种行为既不是错误也不是 Tasks 或 WaitAll() 独有的。 Threads 也会发生同样的事情。
  • 如果关于比赛条件的事情对你来说仍然是个谜,汤姆斯科特不久前制作了一个视频,他(除其他外)给出了一个同时增加数字时可能的比赛条件的例子,这很简单了解:youtube.com/watch?v=RY_2gElt3SA

标签: c# task-parallel-library


【解决方案1】:

正如 elgonzo 在他的评论中指出的那样。 taskCounter++taskCounter-- 这两个操作不是原子的,这意味着它们实际上由几个步骤组成(从内存中读取变量,对其进行加减运算,然后将结果写回内存。

如果多个线程同时执行这些序列,最终结果可能会变得不正确。 假设线程 1 和线程 2 从内存中读取变量同时独立地增加它们各自的副本,然后尝试将结果写回内存。在这种情况下,写入内存的最终值与这两个序列一个接一个地执行不同。

现代 CPU 的高速缓存使得那些并行执行的操作很可能产生不正确的结果。如果按照建议使用Interlocked.Increment / Interlocked.Decrement,您将获得一致的结果。这两个操作使用硬件支持来保证原子性。

【讨论】:

    猜你喜欢
    • 2011-03-14
    • 2022-06-22
    • 2022-01-14
    • 1970-01-01
    • 2012-09-01
    • 2016-10-22
    • 1970-01-01
    • 2023-01-20
    • 2016-02-11
    相关资源
    最近更新 更多