【C#多线程】2.线程池简述+两种传统的异步模式
线程池简述+两种传统的异步编程模式
1.线程池简述
首先我们要明确一点,编程中讲的线程与平时我们形容CPU几核几线程中的线程是不一样的,CPU线程是指逻辑处理器,比如4核8线程,讲的是这个cpu有8个逻辑处理器,可以同时处理8个线程。我们编程中讲的线程在计算机中可以有许多许多,如下图所示,这些线程并不是都在执行状态,他们平时大部分都是休眠状态,只有进程去调用他们时,他们才是激活状态。线程通过他们的ThreadState(线程状态)属性告诉CPU,它们是否需要被CPU去执行。比如有2000个线程,其中有20个线程的线程状态属性为“待执行”,那么CPU的逻辑处理器就会在空闲时根据线程的优先级去执行线程(4核8线程的CPU最多同时执行8个线程),正在执行的线程状态属性会被改为“正在执行”,当该线程执行结束后,其线程状态属性会被改为“休眠”,此时CPU就不会再理他们。
- 什么是C#线程池呢?
顾名思义,线程池就是放线程的池子,我们在运行任意.NET程序时,都会在CLR(你可以把他理解为软件后台)生成一个线程池,池内已经new出来了很多的Thread实例,我们在需要型线程的时候不用自己new,直接从池子里拿现成的Thread实例即可,用完后这个Thread实例会被自动还回线程池!线程池中的线程对象数量与我们计算机有关,具体数字我忘了,反正是CPU核心越多,逻辑处理器越多,那么线程池的线程就越多,我们一般不用管池内有多少个线程(一般是足够你用的),即使线程池的线程都在被占用状态,此时你再从线程池拿线程时,线程池也会自动new新增一个线程给你。
- 为什么要使用C#线程池呢?
因为new一个Thread是比较耗费资源并且执行较慢的行为,比如我们在一个1000次的循环中,每个循环都要new出一个Thread进行某些任务的处理,会使得任务执行缓慢,并且计算机内存蹭蹭上涨。我们不如直接在每次循环中从线程池获取一个线程,用完再放回去,这样的处理不仅速度快,对内存也没有任何影响。
2.线程池的使用(简单讲解)
因为线程池在.NET4.0后新出的Task类及Async与await关键字出现后就不怎么用了,这里仅仅简单讲一讲线程池的用法。
直接看代码:
//创建一个线程执行的方法
public static void DoSth(object obj)
{
//输出当前执行线程的ID
Console.WriteLine((string)obj+Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);//线程睡眠5秒
}
static void Main(string[] args)
{
for (int i = 0; i < 1000; i++)
{
//-----------------非简写方式-----------------
//WaitCallback是一个委托(有一个Object类型参数,无返回值)
WaitCallback callBack = new WaitCallback(DoSth);
//QueueUserWorkItem只支持WaitCallback作为参数,第二个参数是传入委托方法的参数
ThreadPool.QueueUserWorkItem(callBack, "abc");
//-----------------lambda简写方式-----------------
ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
{
//输出当前执行线程的ID
Console.WriteLine((string)obj + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(500);//线程睡眠5秒
}),"abc");
}
}
这里补充讲一下ThreadPool.QueueUserWorkItem方法,这个方法从作用上讲是从线程池获取一个新线程去执行一个委托方法。但是为什么它的方法名是QueueUserWorkItem而非GetValueableThread这样的名称呢?因为QueueUserWorkItem的实质其实是将委托方法传入线程池的一个任务队列中,线程池中的空闲线程负责去对任务队列中的线程进行执行,这才是它实质的运行逻辑。
注意:ThreadPool.QueueUserWorkItem只能接受“有且只有一个Object类型,且无返回值的委托方法”。
3.异步编程简介
同步编程:我们平时不用多线程的时候基本就是同步编程模式,代码从上到下依次执行。
关于什么是异步编程模式,首先我们看一段代码:
//声明一个执行一次耗费5分钟的方法
public static void Spend5Min()
{
for(int i=0;i<300;i++)
{
Thread.Sleep(1000);
}
}
//主方法
static void Main(string[] args)
{
Thread t1 = new Thread(Spend5Min);
t1.Start();//将Spend5Min()这个方法交给t1线程执行
Spend5Min();//主线程去执行Spend5Min()方法
}
这段代码会怎么运行呢?
首先主线程会将Spend5Min这个方法交给t1线程去运行,然后自己也开始运行Spend5Min这个方法。这时候,主线程与t1线程基本会同时执行Spend5Min方法。
上面这种模式就是异步编程模式,异步编程的实质就是代码并非是从上到下依次执行的,而是在代码中间产生一个新线程分支,去执行新任务,主线程只负责将任务交给他,然后就不管它,继续往下执行。(而不是等t1线程把Spend5Min方法执行完毕后,主线程再开始执行Spen5Min)。
这里有一个的误区,许多人觉得只要用了多线程就是异步编程,请看下面的代码:
static void Main(string[] args)
{
Thread t1 = new Thread(Spend5Min);
t1.Start();//将Spend5Min()这个方法交给t1线程执行
t1.Join();//让主线程等待t1线程执行完毕再继续执行。
Spend5Min();//主线程去执行Spend5Min()方法
}
因为t1.Join方法,让主线程再此处会等待t1线程执行完毕再继续执行,这种编程模式其实依旧是同步编程模式,因为它依旧是从上到下依次执行的,上面这段代码可以说等同于下面这段代码。
static void Main(string[] args)
{
Spend5Min();
Spend5Min();
}
4.传统的异步编程模式APM
C# .NET最早出现的异步编程模式被称为APM(Asynchronous Programming Model)。这种模式主要由一对Begin/End开头的方法组成。BeginXXX方法用于异步启动一个耗时任务,EndXXXEndXXX用来处理BeginXXX所返回的值(IAsyncResult对象)。BeginXXX方法和EndXXX方法之间的信息通过一个IAsyncResult对象来传递,IAsyncResult 对象是异步的核心,简单的说,他是存储异步返回值+异步状态信息的一个接口,也可以用它来结束当前异步。
.NET中一个典型的例子是System.Net命名空间中的HttpWebRequest类里的BeginGetResponse和EndGetResponse这对方法:
IAsyncResult BeginGetResponse(AsyncCallback callback, object state)
上面的BeginGetResponse用来开启一个异步方法,下面这个方法用于处理上面异步方法返回的值,只有执行完了EndXXX,一个完整的异步操作才算完成(EndXXX一般写在Beginxxxx的回调函数中)。
WebResponse EndGetResponse(IAsyncResult asyncResult);
注意: BeginInvoke和EndInvoke必须成对调用.即使不需要返回值,但EndInvoke还是必须调用,否则可能会造成内存泄漏。
APM使用简单明了,虽然代码量稍多,但也在合理范围之内。APM两个最大的缺点是不支持进度报告以及不能方便的“取消”。
示例:
(1)同步调用异步方法
下面代码介绍了APM异步方法的错误用法,虽然使用了异步方法,但是其效果依旧是同步模式,所以称下面的代码是同步方式调用异步方法。
public class Program
{
public delegate int AddHandler(int a, int b);
public static int Add(int a, int b)
{
Thread.Sleep(3000);
return a+b;
Console.WriteLine("异步方法执行完毕");
}
static void Main()
{
AddHandler handler = new AddHandler(Add);
//BeginInvoke: 委托(delegate)的一个异步方法的开始
//第三个函数为回调函数,BeginInvoke完成后自动执行
IAsyncResult result = handler.BeginInvoke(1,2,null,null);
Console.WriteLine("在前面没执行完前我这就执行完了");//在异步方法还没执行完之前,此句代码就会被执行
//返回异步操作结果()
//因为result还没有被异步方法返回,主线程代码会卡在这个地方,直到异步方法把result返回(这就导致与同步代码一样了)
Console.WriteLine(handler.EndInvoke(result));
Console.ReadLine();
}
}
代码解释:handler.BeginInvoke仅仅只负责开始异步执行委托方法,并返回当前异步result对象。只有主动执行handler.EndInvoke(异步result)才可获取到方法return中的结果。
代码效果:可以看到,主线程并没有等待,而是直接向下运行了。但是问题依然存在,当主线程运行到EndInvoke时,如果这时BeginInvoke没有执行结束(result还没被算出来),这时为了等待调用结果,主线程依旧会被阻塞。
(2)正确使用APM异步模式
思路:将handler.EndInvoke放在handler.BeginInvoke的回调函数中执行,这样当BeginInvoke执行完毕后,后台线程继续执行回调函数(包括handler.EndInvoke方法)直接输出结果,不会阻塞主线程。
public class Program
{
public delegate int AddHandler(int a, int b);
public static int Add(int a, int b)
{
Thread.Sleep(3000);
return a+b;
Console.WriteLine("异步方法执行完毕");
}
static void Main()
{
AddHandler handler = new AddHandler(Add);
//第三个函数为回调函数,BeginInvoke完成后自动执行
//第四个函数定义异步执行result完成后的状态
IAsyncResult result = handler.BeginInvoke(1,2,new AsyncCallback(MyCallback),"AsycState:OK");
Console.WriteLine("在前面没执行完前我这就执行完了");
Console.ReadLine();
}
//异步回调:异步中执行的回调函数
static void MyCallback(IAsyncResult result)
{
//result 是“加法Add()方法”的返回值
//AsyncResult 是IAsyncResult接口的一个实现类,要引用命名空间:System.Runtime.Remoting.Messaging
//AsyncDelegate 属性可以强制转换为用户定义的委托的实际类。
AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate;
Console.WriteLine(handler.EndInvoke(result));
Console.WriteLine(result.AsyncState);
}
}
补充:因为委托的BeginInvoke中第4个参数可以放入任意对象,一般用于包含关于异步操作的信息,所以为了简化回调函数,我们可以直接将委托对象传递到回调函数:
IAsyncResult result = handler.BeginInvoke(1,2,new AsyncCallback(AddComplete),AddHandler);
这时result.AsyncState就装着AddHandler委托对象了,回调函数可简化为:
static void AddComplete(IAsyncResult result)
{
AddHandler handler = (AddHandler)result.AsyncState;
Console.WriteLine(handler.EndInvoke(result));
。。。。。
}
补充:如何在普通方法中创建回调函数?代码如下:
public void Method(参数1,参数2,Action<string> CallBackHandler)
{
//正常执行
string result = ...;//得到结果
//将结果传入回调函数中
CallBackHandler.Invoke(result);
}
5.传统的异步编程模式EAP
在C# .NET第二个版本中,增加了一种新的异步编程模型EAP(Event-based Asynchronous Pattern),EAP模式的异步代码中,典型特征是一个以"Async"结尾的"方法"和以"Completed"结尾的"事件"。XXXCompleted事件将在异步处理完成时被触发,在事件的处理函数中可以操作异步方法的结果。往往在EAP代码中还会存在名为CancelAsync的方法用来取消异步操作,以及一个ProgressChenged结尾的事件用来汇报操作进度。通过这种方式支持取消和进度汇报也是EAP比APM更有优势的地方。EAP中取消机制没有可延续性,并且不是很通用。
.NET2.0中新增的BackgroundWorker可以看作EAP模式的一个例子。另一个使用EAP的例子是被HttpClient所取代的WebClient类(新代码应该使用HttpClient而不是WebClient)。WebClient类中通过DownloadStringAsync方法开启一个异步任务,并有DownloadStringCompleted事件供设置回调函数,还能通过CancelAsync方法取消异步任务。
因为APM与EAP异步编程模式目前在新代码中基本不用了,所以这里就随便讲讲,后续博客中将详细的讲解对基于Task及Async与await关键字的TAP异步模式。