一、什么是同步?什么是异步?
在 .NET 的世界里,async 和 await 这两个关键字可以说是异步编程的门面担当。async 用来标记一个方法是异步方法,里面可以藏着多个 await 表达式;而 await 呢,就是专门在异步方法里等待某个任务(Task 或 Taskawait 时,当前方法会暂停,但主线程并不会傻等着——它会转身去干别的事,等那个任务完成了再回来继续跑剩下的代码。
那同步和异步到底怎么区分?
- 同步:一个方法被调用,调用者必须老老实实等到它执行完,才能接着往下走。这就好比排队买奶茶,前面的人不取走,后面的人就干等着。
- 异步:方法一被调用就立即返回,然后启动一个线程(Task)去处理它内部的逻辑,调用者根本不用等。这就相当于你点了外卖,然后该干嘛干嘛,外卖做好了自然会送到你手上。
二、async/await 是怎么提高性能的?
异步最大的好处就是非阻塞——调用线程不会停下来干等子线程完成。所以,那些不急着要结果、但是很耗时的任务,完全可以扔到异步里去跑,整个程序的效率自然就上来了。
举个直观的例子:主线程要依次执行 5 个方法,耗时分别是 0.1s、0.2s、0.3s、0.4s、0.5s。如果用同步的方式,主线程得老老实实一个一个串着跑,总时间就是 1.5 秒。但如果把这些方法都改成异步的,主线程大概只需要 0.5 秒就能搞定。
原因很简单:同步就像一根线穿五颗珠子,一颗穿完才能穿下一颗,总时间就是所有珠子时间之和。异步呢,相当于五个人每人拿一根线同时穿一颗珠子,总时间取决于最慢的那颗珠子——0.5 秒。因为主线程调用异步方法时,不会一头扎进去执行,而是在遇到 await 后立刻返回一个 Task,这个 Task 会在子线程里慢慢跑,主线程则继续往后走,去调用下一个方法。
那问题来了:主线程跑得飞快,异步方法的结果怎么收回来?常见的方式有这么几种:
1. 直接用 await 等待
public class Demo
{
public async Task DemoAsync()
{
await Task.Delay(1000);
Console.WriteLine("1秒后执行");
}
}
public Class Program
{
static async Task Main(string[] args)
{
Demo demo = new Demo();
// 使用 await 等待DemoAsync的执行
await demo.DemoAsync();
Console.ReadKey();
}
}
2. 使用事件通知
internal class Program
{
static void Main(string[] args)
{
Demo demo = new Demo();
demo.OnEvent += (() =>
{
Console.WriteLine("事件订阅");
});
demo.DemoAsync();
Console.ReadKey();
}
}
public class Demo
{
public event Action OnEvent;
public async Task DemoAsync()
{
Console.WriteLine("开始执行");
await Task.Delay(1000);
Console.WriteLine("1秒后执行");
OnEvent?.Invoke();
}
}
3. 回调函数
internal class Program
{
static void Main(string[] args)
{
Demo demo = new Demo();
demo.DemoAsync(() =>
{
Console.WriteLine("回调函数");
});
Console.ReadKey();
}
}
public class Demo
{
public async Task DemoAsync(Action callback)
{
Console.WriteLine("开始执行");
await Task.Delay(1000);
callback?.Invoke();
Console.WriteLine("1秒后执行");
}
}
4. 只启动异步操作,不等待结果
如果只是启动一个异步任务,根本不需要它的返回值,那就直接调用,不用加
await。主线程不会被阻塞,也不会去管结果。
internal class Program
{
static void Main(string[] args)
{
Demo demo = new Demo();
demo.DemoAsync();
Console.ReadKey();
}
}
public class Demo
{
public async Task DemoAsync()
{
Console.WriteLine("开始执行");
await Task.Delay(1000);
Console.WriteLine("1秒后执行");
}
}
5. 把异步结果存到变量里,后面再取
如果想在某个时刻拿到异步结果,但又不想立刻等它完成,可以先把返回的 Task 存起来,等需要的时候再用
Wait()或.Result来取。
internal class Program
{
static void Main(string[] args)
{
Demo demo = new Demo();
var demoResult = demo.DemoAsync();
// 使用 wait等待异步的完成
demoResult.Wait();
if (demoResult.IsCompleted)
{
Console.WriteLine("str执行完成");
Console.WriteLine(demoResult.Result);
}
Console.ReadKey();
}
}
public class Demo
{
public async Task DemoAsync()
{
Console.WriteLine("开始执行");
await Task.Delay(1000);
Console.WriteLine("1秒后执行");
return "异步返回结果";
}
}
三、异步到底解决了什么?到底起到了什么样的作用?
- 提高响应性:用
async和await可以避免那些长时间运行的操作(比如 IO 操作)把主线程卡死,应用程序的反应自然更快了。 - 简化异步代码:写异步代码现在几乎和写同步代码一样直白,出错概率也大大降低。
- 优化资源使用:线程在等待任务完成时会被释放出来,跑去服务别的任务,而不是傻等——资源利用率一下就上去了。
四、在使用异步时的一些问题的解决
1、异步的传递性问题
异步的效果会沿着调用链一路向上传染。你调了个异步方法,那调用它的方法也得变成异步,而且返回类型还得改成 Task 或 Task
那怎么避免这种“强制传染”呢?有几种思路:
- 不用 await 直接调用:如果不需要异步方法的结果,可以直接调用,不加
await,这样调用者就不用变成异步方法了。 - 用 Task.Run 来启动异步操作:
Task.Run会新起一个任务去跑异步逻辑,从而切断了传递性。 - 用事件或回调来处理结果:如果只需要在完成时得到通知,事件或回调比直接
await更灵活,也不会逼迫上层变成异步。 - 把异步结果存起来,后面再等:将 Task 赋值给一个变量,等需要结果时再调用
Wait()或访问.Result,虽然不优雅但确实能避开传递性。