首页 > 解决方案 > 证明 int++ 不是原子的可靠方法

问题描述

我已经可以看出这不是由于增量不正确,而是我似乎无法理解的一小部分难题。

我们有以下代码:

internal class StupidObject
{
    static public SemaphoreSlim semaphore = new SemaphoreSlim(0, 100);

    private int counter;

    public bool MethodCall() => counter++ == 0;

    public int GetCounter() => counter;
}

以及下面的测试代码来尝试看看它是否是一个原子操作:

var sharedObj = new StupidObject();
var resultTasks = new Task[100];
for (int i = 0; i < 100; i++)
{
    resultTasks[i] = Task.Run(async () =>
    {
        await StupidObject.semaphore.WaitAsync();
        if (sharedObj.MethodCall())
        {
            Console.WriteLine("True");
        };
    });
}
Console.WriteLine("Done");

Console.ReadLine();


StupidObject.semaphore.Release(100);

Console.ReadLine();

Console.WriteLine(sharedObj.GetCounter());

Console.ReadLine();

我希望看到多个True's 写入控制台,但我曾经看到一个。

这是为什么?据我了解,++操作读取值,增加读取值,然后将该值存储到变量中。

那是3个操作。如果我们有竞争条件,线程A会执行以下操作:

另一个线程B做了同样的事情,但是将线程击败A到第三个操作,如下所示:

A完成写入递增的读取值时,它应该在完成写入操作后打印回来0,与线程相同B

我是否在设计方面遗漏了一些东西,或者我的测试不足以使这种确切情况成为现实?

True没有任务并行库的示例(仍然向控制台生成一个):

var sharedObj = new StupidObject();
var resultTasks = new Thread[10000];
for (int i = 0; i < 10000; i++)
{
    resultTasks[i] = new Thread(() =>
    {
        StupidObject.semaphore.Wait();
        if (sharedObj.MethodCall())
        {
            Console.WriteLine("True");
        };
    });
    resultTasks[i].IsBackground = false;
    resultTasks[i].Start();
}
Console.WriteLine("Done");

Console.ReadLine();


StupidObject.semaphore.Release(10000);

标签: c#multithreadingasynchronous.net-coreasync-await

解决方案


利亚姆所说的 Console.WriteLine是可能的,但还有另一件事。

启动任务不等于启动线程,即使启动线程也不能保证所有线程都会立即启动。启动 100 个短任务可能甚至不会显着填满 .Net 的线程池,因为这些任务很快结束,线程池的管理器可能不会启动超过 3-5 个线程。这不是您想要开始并行 100 增量以相互竞争时想要看到的“立即”和“并行”,对吗?请记住,任务首先排队,然后分配给线程。

请注意,StupidObject 的计数器从零开始,这是该值为零的唯一时刻。如果任何线程赢得比赛并成功写入该整数的更新,您将在所有未来任务中获得 FALSE,因为它已经是1.

如果线程池队列中有很多任务,首先必须注意这一事实。在程序开始时,线程池缺少线程。他们不是在程序开始时就开始几十个。它们是按需启动的。很可能你用 100 个任务填满了队列,创建了线程池的线程,选择第一个任务,将计数器颠倒到 1,然后线程池可能会启动新线程以更快地使用任务。

为了更好地了解正在发生的事情,而不是打印出“true”,而是收集观察到的值return counter++:让每个任务运行,完成,将其值存储在 Task's.Result中,然后运行线程/任务,然后等待所有这些都停止,然后收集 .Results 并写出这些值的直方图。即使您没有看到 5 个零,也可能会看到 3 个 1、7 个 2、2 个 3 等等。


推荐阅读