异步(Asynchronos),正如其名称,是同步(Synchronos)的对应。很多新手往往会把异步与多线程划等号,事实上并非如此。尽管有些异步操作涉及多线程,异步和多线程并没有必然的关系,我们应当分开进行理解。
说起同步,大家一定相当了解。我们平时写的普通的控制台程序一般就是同步的。如果您是大学生,一般基础的c语言课程中写的所有程序也是同步的。同步操作往往易于理解,方便设计。而异步操作则在设计上稍微困难一些,但是用的好则能大幅度提升程序性能。
异步代码一度相当难以构建,因为他涉及各种回调地设计,可以使代码变得相当复杂。但是c#作为Task-based Asynchronous Pattern (TAP) 实现的鼻祖之一,可以利用Task
快速而轻松地实现异步操作。本文将仅介绍TAP
在TAP中,一个异步操作被抽象为Task
,若它有返回值则是Task<T>
,定义一个最简单的异步方法你只需要:
async Task DoAsync()
{
}
在某个方法前加上async
关键字,并且让它拥有Task<T>
或Task
类型的返回值。这样,你就成功的构造了一个异步方法。
在构造成功后,使用异步方法时,你可以:
async Task Caller()
{
await DoAsync();
}
注意,这里使用了await
关键字。这个关键字会强迫调用DoAsync
函数的线程等待DoAsync
方法执行完毕。await
关键字只能在异步方法中使用
在MSDOCS中,TAP的教程编写者用做早饭写了个很详细的例子,若您对TAP缺少最基础的了解,请点击此处
看到这里,我将假设您已经了解了TAP的基础使用。如果您没有,请返回查看上节里的教程链接。
虽然MSDOCS的教程里的例子写的非常形象,易于理解。但是这个例子有一定的误导性。看完这个文章的很多新手容易将异步方法与多线程划等号,认为所谓异步方法就是额外开一个线程执行的方法,实际上并非如此。我将举例说明。
以上一节的两个方法举例,我们来分析一下他的执行过程:
Caller
开始执行,使用线程id为1
Caller
调用DoAsync
方法并等待,进入DoAsync
方法后,查看所在线程,仍然为1
DoAsync
执行完毕,返回Caller
,查看所在线程,仍然为1
看到这个过程,可能就有人不理解了。但是我可没有乱说,不信的话我们可以加断点,查看Thread.CurrentThread.ManagedThreadId
的值,我敢打赌它在Caller执行开始后就没有变过。也就是说上方的方法,完全是在一个线程执行的,和普通的方法几乎没有任何区别(唯一的区别是:多生成了两个Task
,浪费更多性能)
这时候可能有人就会说异步骗人了。其实并不是异步骗人,是我们的使用方法不对。如果我们真的想要开一个新的线程,我们应该把这样:
async Task DoAsync()
{
await Task.Yield();
}
这样的话,你会看到await Task.Yield();
前后是不一样的线程。await Task.Yield();
实际上是一个语法糖,它等效于
async Task Caller()
{
Task.Run(async ()=>await DoAsync());
}
所以,其实所有的异步方法可以分为两种:真异步和假异步,假异步和同步方法是一样的。也就是说
async Task DoAsync()
{
Thread.Sleep(1000);
Console.WriteLine(2);
}
async Task Caller()
{
await DoAsync();
Console.WriteLine(1);
Console.ReadKey();
}
和
async Task DoAsync()
{
Thread.Sleep(1000);
Console.WriteLine(2);
}
async Task Caller()
{
//去除await
DoAsync();
Console.WriteLine(1);
Console.ReadKey();
}
效果实际是一样的,都是先输出2再输出1.
判断一个方法是否是假异步有以下的技巧:
所有别人封装的异步方法一定是真异步(除非封装者完全不懂异步,这种概率很小)
所有调用真异步方法并await的异步方法也是真异步方法
Task.Run()
是个真异步方法
Task.Factory.StartNew()
是个真异步方法
上方,我举例时拿多线程说明了真异步和假异步的区别。但是需注意,真异步不一定是多线程的。除了使用Task.Run()
构建的多线程真异步,还有一种很常见的异步,就是os操作异步。
在现代电脑上,io操作一般由os进行,而不是应用程序本身。所以io操作实际上并不占用程序本身的运算资源。因此,c#的开发者给很多基础的io操作设计了异步版本,让程序能够在os进行io操作时进行其他的活动。
async Task DoAsync()
{
//换为由os计时的Delay
await Task.Delay(1000);
Console.WriteLine(2);
}
async Task Caller()
{
//去除await
DoAsync();
Console.WriteLine(1);
Console.ReadKey();
}
把我们上节举例的代码替换为上方的代码,你就会发现1比2先打印出来。因为await Task.Delay(1000);
会通知os来计时,在此时我们的程序的当前线程是可以继续执行其他任务的--也就是Console.WriteLine(1);
整体过程解释如下:
开始执行Caller
,线程id为1.
进入DoAsync
方法,线程id仍然是1.
线程1调用await Task.Delay(1000);
,此操作交由os执行,线程1控制权被释放
操作Console.WriteLine(1);
发现线程1可用,使用它打印1.
操作Console.ReadKey();
在线程1上继续执行,开始等待用户输入
os执行await Task.Delay(1000);
完毕,回调.net程序,通知DoAsync
继续执行。
DoAsync
请求线程池,线程池提供可用线程(不可能是线程1,线程1正在等待用户输入(Console.ReadKey();
)),继续执行,打印2.
尽管TAP强大无比,但是错误的使用它可能造成死锁。这部分我先占个坑,等待以后补充。
以上就是c#的异步操作需要注意的文章,如果对您有帮助,点一个👍吧
其他教程
本文章使用limfx的vsocde插件快速发布