简体   繁体   中英

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 . 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*. 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 am unable to produce this scenario in practice though. The code above always prints OK in my machine. Does this mean that the code is thread safe? Or I should synchronize the access to the i variable using a lock ?

(* one thread switch occurs every 2 iterations on average, according to my tests)

The problem with thread safety is about reading/writing 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. 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. 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. That's handled automatically by the Framework. When the async method is about to suspend, the infrastructure captures an ExecutionContext. The delegate that gets passed to the awaiter has a reference to this ExecutionContext instance and will use it when resuming the method. This is what enables the important “ambient” information represented by ExecutionContext to flow across awaits.

Worth noting that the YieldAwaitable returned by Task.Yield() always returns false .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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