C#异步操作是否需要加锁?异步多线程竞态问题解析
异步操作中的共享变量竞态问题
问题描述
我编写了一段C#异步代码,预期共享变量a的最终值是50000(5个任务各执行10000次自增),但实际运行结果往往小于等于这个值。一开始误以为异步操作只会在单线程运行,直到添加线程ID打印后,发现任务结束时运行在不同线程,才意识到之前的误解,想知道具体原因。
原始代码
class A { public int a = 0; public async Task Test1() { Console.WriteLine("Test1 START"); for (long i = 0; i < 10000; ++i) { a++; await Task.Yield(); } Console.WriteLine("Test1 END"); } } class Program { static void Main(string[] args) { A a = new A(); Task t1 = a.Test1(); Console.WriteLine("1"); Task t2 = a.Test1(); Console.WriteLine("2"); Task t3 = a.Test1(); Console.WriteLine("3"); Task t4 = a.Test1(); Console.WriteLine("4"); Task t5 = a.Test1(); Console.WriteLine("5"); while (!t1.IsCompleted || !t2.IsCompleted || !t3.IsCompleted || !t4.IsCompleted || !t5.IsCompleted) { } Console.WriteLine(a.a); Console.WriteLine("Main END"); Console.ReadLine(); } }
修改后的代码(添加线程ID打印)
public async Task Test1() { Console.WriteLine("Start Thread ID : " + Thread.CurrentThread.ManagedThreadId); for (long i = 0; i < 10000; ++i) { a++; await Task.Yield(); } Console.WriteLine("END Thread ID : " + Thread.CurrentThread.ManagedThreadId); }
运行结果
Start Thread ID : 1 1 Start Thread ID : 1 2 Start Thread ID : 1 3 Start Thread ID : 1 4 Start Thread ID : 1 5 END Thread ID : 14 END Thread ID : 14 END Thread ID : 5 END Thread ID : 6 END Thread ID : 6 50000 Main END
原因解析
async/await不绑定单线程:控制台程序没有SynchronizationContext,await完成后,后续代码会由线程池调度,可能在任意线程池线程上执行。await Task.Yield()会强制让出当前线程,把后续循环迭代的操作放到线程池队列,所以每个自增操作都可能在不同线程执行。a++不是原子操作:a++实际包含三个步骤:读取当前a的值、加1、写入新值。当多个线程同时执行这个操作时,会出现竞态条件:比如线程A读取a=10,线程B也读取a=10,两者都加1到11,然后写入,最终a只增加了1,而不是2,这就导致最终结果小于预期的50000。- 空循环等待的偶然性:主函数里的空
while循环会占用主线程资源,这次运行结果碰巧是50000只是偶然情况,多次运行大概率会出现值偏小的情况。
内容的提问来源于stack exchange,提问作者박재한
相关产品推荐
相关产品推荐

