C# 初见异步方法 满满的全是坑

初见

虽然写了不少的C#代码,但却还么有使用过带有async关键字的异步方法。平常遇到的需求,使用普通的多线程都可以完美解决,感觉异步方法只是微软想当然开发出来的,虽然华丽但是毫无用处的垃圾功能。直到有一天,我要修改一个几乎全是异步方法的代码。

这里介绍一下本次修改遇到的全部的坑。可以通过这些坑纠正我们的认知偏差。

坑1 何时异步执行

请查看为何下面两个代码输出内容不同:

internal class Program
{public static void Main(){Program p = new Program();_ = p.AsyncTask();Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);}private async Task AsyncTask(){Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);await Task.Delay(1);}
}

输出内容:

执行异步方法 id:1
主线程退出 id:1
internal class Program
{public static void Main(){Program p = new Program();_ = p.AsyncTask();Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);}private async Task AsyncTask(){await Task.Delay(1);Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);}
}

输出内容:

主线程退出 id:1
执行异步方法 id:6

接下来将上面第二段代码转换一下,就能看出来为什么了。
第二段代码等价于:

internal class Program
{public static void Main(){Program p = new Program();Task task = p.AsyncTask();task.ContinueWith(t => {Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);});Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);}private Task AsyncTask(){return Task.Delay(1);}
}

可以看出,C#的关键字async与await太误导人了。
同时也可以看出异步方法的本质:
在遇到需要等待执行的Task任务,让线程直接执行异步方法的后续代码,同时从线程池中取出新线程执行任务。

坑2 如何在非异步方法中调用异步方法

internal class Program
{public static void Main(){Program p = new Program();_thread = new Thread(p.CheckThreadId);_thread.Start();// 方法结束 交还ui线程}private static Thread? _thread;private async Task CheckThreadId(){for (int i = 0; i < 10; i++){// 执行任务await Task.Delay(1000);}}}
}

C#设计异步方法的主要目标,就是使用Task自己的线程池,免去建立新线程的开销,以及自己搭建线程池的麻烦。可是结合上面的代码看着就很奇怪,既然使用了异步方法,为何还要建立新的线程?最重要的是,无法通过判断_thread的运行状态来判断任务是否还在执行。

于是,我改成了这个样子,然后就出问题了,主线程,也就是winform的ui线程被占用,窗体卡死。
为何会卡死主线程,后面再说。

internal class Program
{public static void Main() // 按键点击后执行的方法{Program p = new Program();_task = p.CheckThreadId();// 方法结束 交还ui线程}private static Task? _task;private async Task CheckThreadId(){for (int i = 0; i < 10; i++){// 执行任务await Task.Delay(1000);}}
}

其实,正确的方式应该是这样的,交给Task来执行异步方法,而不要直接调用异步方法。

internal class Program
{public static void Main() // 按键点击后执行的方法{Program p = new Program();_task = Task.Run(p.CheckThreadId);// 方法结束 交还ui线程}private static Task? _task;private async Task CheckThreadId(){for (int i = 0; i < 10; i++){// 执行任务await Task.Delay(1000);}}
}

坑3 被卡死的ui线程

从该代码中,可以看出循环线程有问题。虽然每次循环都有await,可是任务只能被执行一次。不过由于未知的原因,在第一次await等待结束后,继续接受执行的百分百为ui线程,可能是因为看该线程在限制状态吧。之后的循环,因为await不再生效,所以ui线程就被卡死在其中了,winfrom窗口也被卡死。

internal class Program
{public static void Main() // 按键点击后执行的方法{Program p = new Program();_task = p.Work();Console.WriteLine("主线程结束");// 方法结束 交还ui线程}private static Task? _task;private async Task Work(){Task delayTask = Task.Delay(1000);for (int i = 0; i < 10; i++){// 执行任务Console.WriteLine("执行任务" + DateTime.Now);await delayTask;}}
}