简体   繁体   English

异步代码、共享变量、线程池线程和线程安全

[英]Asynchronous code, shared variables, thread-pool threads and thread safety

When I write asynchronous code with async/await, usually with ConfigureAwait(false) to avoid capturing the context, my code is jumping from one thread-pool thread to the next after each await .当我使用 async/await 编写异步代码时,通常使用ConfigureAwait(false)来避免捕获上下文,我的代码在每个await之后从一个线程池线程跳转到下一个线程。 This raises concerns about thread safety.这引起了对线程安全的担忧。 Is this code safe?这段代码安全吗?

static async Task Main()
{
    int count = 0;
    for (int i = 0; i < 1_000_000; i++)
    {
        Interlocked.Increment(ref count);
        await Task.Yield();
    }
    Console.WriteLine(count == 1_000_000 ? "OK" : "Error");
}

The variable i is unprotected, and is accessed by multiple thread-pool threads*.变量i不受保护,可被多个线程池线程*访问。 Although the pattern of access is non-concurrent, it should be theoretically possible for each thread to increment a locally cached value of i , resulting to more than 1,000,000 iterations.尽管访问模式是非并发的,但理论上每个线程应该可以增加本地缓存的i值,从而导致超过 1,000,000 次迭代。 I am unable to produce this scenario in practice though.虽然我无法在实践中产生这种情况。 The code above always prints OK in my machine.上面的代码总是在我的机器上打印 OK。 Does this mean that the code is thread safe?这是否意味着代码是线程安全的? Or I should synchronize the access to the i variable using a lock ?或者我应该使用lock同步对i变量的访问?

(* one thread switch occurs every 2 iterations on average, according to my tests) (* 根据我的测试,平均每 2 次迭代发生一次线程切换)

The problem with thread safety is about reading/writing memory.线程安全的问题在于读/写 memory。 Even when this could continue on a different thread, nothing here is executed concurrent.即使这可以在不同的线程上继续,这里也不会并发执行。

I believe this article by Stephen Toub can shed some light on this.我相信 Stephen Toub 的这篇文章可以阐明这一点。 In particular, this is a relevant passage about what happens during a context switch:特别是,这是关于上下文切换期间发生的事情的相关段落:

Whenever code awaits an awaitable whose awaiter says it's not yet complete (ie the awaiter's IsCompleted returns false), the method needs to suspend, and it'll resume via a continuation off of the awaiter.每当代码等待一个等待者说它尚未完成的等待对象时(即等待者的 IsCompleted 返回 false),该方法需要暂停,并且它将通过等待者的延续来恢复。 This is one of those asynchronous points I referred to earlier, and thus, ExecutionContext needs to flow from the code issuing the await through to the continuation delegate's execution.这是我之前提到的那些异步点之一,因此,ExecutionContext 需要从发出等待的代码流向延续委托的执行。 That's handled automatically by the Framework.这是由框架自动处理的。 When the async method is about to suspend, the infrastructure captures an ExecutionContext.当异步方法即将挂起时,基础结构会捕获一个 ExecutionContext。 The delegate that gets passed to the awaiter has a reference to this ExecutionContext instance and will use it when resuming the method.传递给等待者的委托具有对此 ExecutionContext 实例的引用,并将在恢复该方法时使用它。 This is what enables the important “ambient” information represented by ExecutionContext to flow across awaits.这就是使 ExecutionContext 表示的重要“环境”信息能够流经等待的原因。

Worth noting that the YieldAwaitable returned by Task.Yield() always returns false .值得注意的是YieldAwaitable Task.Yield()返回的 YieldAwaitable 总是返回false

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM