天天看点

C#异步编程基础入门总结

C#异步编程基础入门总结

参考网址:https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/

1.前言

*.NET Framework提供了执行异步操作的三种模式:

异步编程模型(APM)模式(也称为IAsyncResult的模式),其中异步操作要求Begin和End方法(例如,BeginWrite和EndWrite异步写入操作)。这种模式不再被推荐用于新开发。有关更多信息,请参阅异步编程模型(APM)。

基于事件的异步模式(EAP),它需要一个具有Async后缀的方法,并且还需要一个或多个事件,事件处理程序委托类型和被EventArg派生类型。EAP在.NET Framework 2.0中引入。不再推荐新的开发。有关更多信息,请参阅基于事件的异步模式(EAP)。

基于任务的异步模式(TAP),它使用单一方法来表示异步操作的启动和完成。TAP在.NET Framework 4中引入,是.NET Framework中推荐的异步编程方法。C#中的async和等待关键字,Visual Basic语言中的Async和Await运算符为TAP添加语言支持。有关更多信息,请参阅基于任务的异步模式(TAP)。*

2.异步的应用场景

在计算机程序的运行中,计算是需要一定的时间的,在运算时间过长的任务时,比如上传大文件、读取文件流、数据库操作、httprequest等,如果是同步(synvronous)必须等待该任务执行完成才能继续下一个任务。使用异步(asynchronous)操作,会开启新的线程,不会等待异步操作完成才去执行后面的程序,相比异步编程优点:1.就是出现长时间处理程序时,不会卡界面,用户仍然可以操作UI界面2.提高程序运行效率,节约CPU资源,提供系统吞吐量。

3.进程和线程的关系

这个面试的时候基本上都会问到,简而言之就是:

一个程序都会有一个进程和一个线程,进程是由CPU进行调度分配资源的,有一个完整的虚拟地址空间,不依赖线程独立存在,反之线程是由进程来调度分配的,只是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享该进程的所有资源。打个简单的比方就像是线程就好比是人体的寄生虫,不能独立存在,必须依靠人(进程)的营养(资源)来生存(执行)

4.异步和多线程的区别

异步是相对同步而言的,我们知道异步是开启了新线程,但是和多线程不是一个概念,异步相当于一个人的“大脑”能够做试卷,又能够看电影,同时处理两件以上不同的事情。多线程好比多个人做不同的事情。

异步操作的本质

c#中异步和多线程的区别是什么呢?异步和多线程两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。 所有的程序最终都会由计算机硬件来执行,无须消耗CPU时间的I/O操作正是异步操作的硬件基础。

线程的本质

线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度

异步操作的优缺点

因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些出入,而且难以调试。

多线程的优缺点

  多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

适用范围

当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。

而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。

5.C#异步方式之一( BeginInvoke、EndInvoke方法)

方式1:使用回调方法完成异步委托

先来看个例子,委托的异步调用,这个例子首先定义一个string类型的返回值、string类型的参数的委托。虽然这中模式不推荐被使用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

class

Program

{

delegate

string

SayHi(

string

name);

//定义委托

static

void

Main(

string

[] args)

{

SayHi sayhi = 

new

SayHi(SayHiName);

//实例化委托

sayhi(

"科比"

);

//一般的直接调用

sayhi.Invoke(

"张林"

);

//使用Invoke方法同步调用

//异步调用

sayhi.BeginInvoke(

"杜兰特"

, (IAsyncResult ar) =>

{

sayhi.EndInvoke(ar);

Console.WriteLine(

"打招呼成功结束"

);

}, 

null

);

}

public

static

string

SayHiName(

string

name)

{

return

"how are you"

+name + 

"?"

;

}

}

  

前两种调用委托的方式都是同步的,BeginInvoke方法的返回值是IAsyncResult类型的

该方法的参数由两部分组成,前面(n)个参数是委托的参数,倒数第二个参数也表示一个委托,该委托是.net系统定义的委托(和func、action类似),查看AsyncCallback的定义如图:

作用就是:作为执行调用的回调方法,值得注意的是,在回调方法中,必须调用EndInvoke方法结束异步调用,EndInvoke是获取异步调用的结果

上面的例子调试的结果如图:

方式2:使用轮询

我们把BeginInvoke的委托参数为null,使用轮询的方式

Func<

string

string

> func = 

delegate

(

string

name)

{

Thread.Sleep(2000);

return

"how are you"

+ name + 

""

;

};

IAsyncResult ar = func.BeginInvoke(

"张林"

,

null

,

null

);

int

i = 1;

while

(!ar.IsCompleted)

{

Console.WriteLine(200*i);

i++;

Thread.Sleep(200);

}

string

result = func.EndInvoke(ar);

Console.WriteLine(result);

结果如图:

6.C#异步方式之二 await async

async和await是一对关键字,它是.net 4.5的特性。在实际工作中使用方便灵活,主要原因就是可以像写同步方法那样去异步编程,代码结构清晰,不用关心如何实现异步的编程。

这里其实要注意的是,之前刚说了异步是开启新的线程来实现的,但是await 和async两个关键字并没有开启新的线程,为了证明这一点,下面建了一个winform的程序,异步获取图片并显示到picturebox上。

public

Form1()

{

InitializeComponent();

this

.label1.Text = 

"主线程Id:"

+Thread.CurrentThread.ManagedThreadId;

}

private

async 

void

button1_Click(

object

sender, EventArgs e)

{

string

imageUrl = 

"https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=3850265187,1181041963&fm=173&s=62E19A4722716A371EB097FB03009015&w=218&h=146&img.JPEG"

;

HttpClient client = 

new

HttpClient();

var

response =await  client.GetAsync(imageUrl);

if

(response.StatusCode == System.Net.HttpStatusCode.OK)

{

var

stream =await response.Content.ReadAsStreamAsync();

Image  image = Bitmap.FromStream(stream,

true

);

this

.label2.Text = 

"线程Id:"

+ Thread.CurrentThread.ManagedThreadId;

this

.pictureBox1.BackgroundImage = image;

}

}

其实不用看图就已经知道答案了,程序运行时不报异常,就已经说明一点:await async两个关键根本创建新的线程。这个涉及到异步更新UI到主线程,就不多说了。

结果如图:

async await方法的使用说明:

  • 返回类型: void 、Task、Task<泛型类型>
  • async、await不会创建新的线程,实现等待的效果,必须同时使用
  • 使用该方法的方法主体也要用async关键字

异步方法事例:

private

static

async Task<

int

> GetValueAsync(

int

a)

{

//Task.run 开启了新的线程

await  Task.Run(() =>

{

Thread.Sleep(2000); 

//模拟耗时

Console.WriteLine(

"GetValueAsync方法结束,线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

return

a * a;

});

Console.WriteLine(

"线程ID:"

+ Thread.CurrentThread.ManagedThreadId+

"异步方法结束"

);

return

a * a;

}

调用异步方法:

private

async 

void

button1_Click(

object

sender, EventArgs e)

{

int

result =await GetValueAsync(5);

this

.label1.Text = 

"异步计算的结果"

+ result + 

"线程ID:"

+ Thread.CurrentThread.ManagedThreadId;

}<br>

7.C#异步方式之三 浅谈Task

  前面刚刚了解到async await是.net 4.5出的特性,Task是.net4.0新出的特性,用来处理异步编程的,其实我们要知道真正实现的异步操作还是Task新增线程来实现的,但是不代表说开一个Task,就开一个线程,有可能是几个Task在一个线程上运行的,他们并不是一一对应的关系,充分利用线程,下面的事例就已经能够说明这一点 

Task创建

Task创建有两种方式一种通过任务工厂赋值立即运行,一种是直接实例化。下面这个例子创建了10个Task

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

static

void

Main(

string

[] args)

{

//启用线程池中的线程异步执行

Task t1 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task1启动...线程ID:"

+Thread.CurrentThread.ManagedThreadId);

});

Task t2 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task2启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

//new 实例化启动

Task t3 = 

new

Task(() =>

{

Console.WriteLine(

"Task3启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

t3.Start();

Task t4 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task4启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t5 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task5启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t6 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task6启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t7 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task7启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t8 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task8启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t9 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task9启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Task t10 = Task.Factory.StartNew(() =>

{

Console.WriteLine(

"Task10启动...线程ID:"

+ Thread.CurrentThread.ManagedThreadId);

});

Console.ReadLine();

}

创建的10个Task,我们从结果中也证明了Task和线程并不是一一对应的关系,结果如图:

Task构造函数

Task状态

我们创建一个task,调用他的Start、Wait方法

static

void

Main(

string

[] args)

{

var

task = 

new

Task(()=> {

Console.WriteLine(

"Task创建成功"

);

});

Console.WriteLine(

"task未开始:"

+task.Status);

task.Start();

Console.WriteLine(

"task已经开始:"

+task.Status);

task.Wait();

Console.WriteLine(

"task已经等待:"

+task.Status);

}

我们从图中可以知道,Task的生命周期如下:

Created:在已经实例化未Start之前的状态

WaittingToRun:表示等待分配线程给Task执行

RanToCompletion:任务执行完毕

Task等待任务结果

1.Task.WaitAll从这个字面意思就知道等待所有任务执行完成,和上面例子Wait方法等待一个任务执行完成很相似,我们来看一个代码:

var

task1 = 

new

Task(() =>

{

System.Threading.Thread.Sleep(3000);

Console.WriteLine(

"task1Created"

);

});

var

task2 = 

new

Task(() =>

{

System.Threading.Thread.Sleep(3000);

Console.WriteLine(

"task2Created"

);

});

task1.Start();

task2.Start();

Task.WaitAll(task1, task2);

Console.WriteLine(

"所有任务执行完!"

);

Console.Read();

结果输出:

task1Created

task2Created

所有任务执行完

除了WaitAll方法还有这些常用的方法

  • Task.WaitAny:等待任何一个任务向下执行
  • Task.ContinueWith等待第一个Task完成自动启动,触发下一个Task,也就是当做任务完成时触发的回调方法
  • Task.GetAwaiter().OnCompleted(Action action) :GetAwaiter 方法获取任务的等待者,调用OnCompleted事件,任务完成时触发

Task任务取消

static

void

Main(

string

[] args)

{

var

source = 

new

CancellationTokenSource();

var

token = source.Token;

Task t1 = Task.Run(() =>

{

Thread.Sleep(2000);

if

(token.IsCancellationRequested)

{

Console.WriteLine(

"任务已取消"

);

}

Thread.Sleep(1000);

},token);

Console.WriteLine(t1.Status);

//取消任务

source.Cancel();

Console.WriteLine(t1.Status);

Console.ReadLine();

}