本文的目的是为了让大家了解什么是异步?什么是多线程?如何实现多线程?对于当前C#当中三种实现多线程的方法如何实现和使用?什么情景下选用哪一技术更好?
第一部分主要介绍在C#中异步(async/await)和多线程的区别,以及async/await使用方法。
第二部分主要介绍在C#多线程当中Thread、ThreadPool、Task区别和使用方法。
-------------------------------------------------------------------------------------------------------------------------
async/await这里的异步只是一种编程模式,一个编程接口设计为异步的,大多数时候都是为了灵活地处理并发流程需求的,对于async/await用法请看以下代码:
static void Main(string[] args) { _ = Async1(); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); } static async Task Async1() { Console.WriteLine("异步开始"); var r = await Async2(); var x = await Async3(r); Console.WriteLine("结果是 {0}", r + x); } static async Task<int> Async2() { await Task.Delay(1000);//一种异步延迟方法 return 100; } static async Task<int> Async3(int x) { await Task.Delay(1000); return x % 7; }
执行结果:
使用async关键字修饰的方法为异步方法,async关键字要和await关键字一同使用才会生效。通过这个程序运行结果我们可以看到对于async/await方法的异步是在遇到await关键字时开始的,如果你编写的代码中只用到了async关键字修饰方法,但是没有用到await关键字,那么此方法执行起来与普通方法一样都是顺序执行的。
使用async/await方法可以实现异步,但我个人觉得从代码阅读的难易程度上来说,使用async/await关键字的代码更难以阅读,我更推荐使用Task来实现异步,后续会详细介绍Task。
使用async修饰的方法返回值有三种类型void,Task,Task<T>,根据返回值类型我认为其实async/await的实现是基于Task的(个人的理解我并没有在任何书籍或者官方资料中看到这样的说法,欢迎交流),说完了async/await异步编程模式再来说一下在C#中三个多线程实现异步的方法的方法Thread,ThreadPool,Task。
按照他们在C#中发布的顺序先来说一下Thread,使用Thread实现以上的功能代码要如何编写呢?我们看一下实例:
static void Main(string[] args) { Thread thread = new Thread(Fun1); //Thread thread = new Thread(() => Fun1(0)); 多线程调用时有参数传递的写法 Console.WriteLine("异步开始"); //thread.IsBackground = true; Thread默认是前台线程,IsBackground = true设置为后台线程 thread.Start(); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); } static void Fun1() { var r = Fun2(); var x = Fun3(r); Console.WriteLine("结果是 {0}", r + x); } static int Fun2() { Thread.Sleep(1000); return 100; } static int Fun3(int x) { Thread.Sleep(1000); return x % 7; }
执行结果:
Thread的使用方法如上,新建一个线程会有一定的内存消耗(线程什么都不做的情况下大约消耗1M)也需要一定的时间,Thread默认是前台线程,前台线程就是当程序主线程结束时会等待前台线程结束返回后主线程才结束,后台线程是当主线程结束时后台线程直接结束,主线程不会等待后台线程结束。当调用start方法时才开始执行Thread多线程方法。对于Thread多线程参数的传递方法一,首先参数的类型必须是object,其次通过Start方法传递参数。方法二,我更推荐通过以上代码中注释的写法通过Lambda表达式实现。
终止线程方法:t.Abort(); 此方法是通过向t线程中抛出异常的方式强制终止线程,我们可以在线程中捕获此异常(ThreadAbortException)系统在finally 子句的结尾处会再次引发ThreadAbortException 异常,如果没有finally 子句,则会在Catch 子句的结尾处再次引发该异常。为了避免再次引发异常,可以在finally 子句的结尾处或者Catch 子句的结尾处调用System.Threading.Thread.ResetAbort 方法防止系统再次引发该异常。注:此方法不支持.Net Core 3.0,不知道为啥各种终止线程的方法在.Net Core 3.0都不支持,可能是个坑。
合并线程方法:t2.Join(); Join 方法用于把两个并行执行的线程合并为一个单个的线程。如果一个线程t1 在执行的过程中需要等待另一个线程t2 结束后才能继续执行,可以在t1 的程序模块中调用t2 的join()方法。这样t1 在执行到t2.Join()语句后就会处于阻塞状态,直到t2 结束后才会继续执行。但是假如t2 一直不结束,那么等待就没有意义了。为了解决这个问题,可以在调用t2 的Join 方法的时候指定一个等待时间,这样t1 这个线程就不会一直等待下去了。例如,如果希望将t2 合并到t1 后,t1 只等待100 毫秒,然后不论t2 是否结束,t1 都继续执行,就可以在t1中加上语句:t2.Join(100)。注:貌似这个join方法也不支持.Net Core 3.0。
接下来介绍一下ThreadPool线程池,就像我上面介绍Thread新建线程是需要消耗一定的时间和内存的。举个例子如果把线程比作小汽车那么new Thread就好比是造一辆新车拿来用,而ThreadPool就好比是一个租车行,需要用车可以去租一个,用完还给租车行当有其它人来租车继续租出去。这样就节省了频繁new Thread造车的开支。基于以上ThreadPool的特性我们不难看出来对于线程池的特性适合于需要频繁新建线程并且每个线程使用的时间较短的场景,例如C/S模式客户端访问服务端。线程池使用方法实例如下:
static void Main(string[] args) { Console.WriteLine("主线程执行!"); ThreadPool.SetMinThreads(1, 1);//设置线程池最小线程数 ThreadPool.SetMaxThreads(5, 5);//设置线程池最大线程数 //参数一:线程池按需创建的最小工作线程数。参数二:线程池按需创建的最小异步I/O线程数。 for (int i = 1; i <= 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(testFun), i); } Console.WriteLine("主线程结束!"); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); } public static void testFun(object obj) { Console.WriteLine(string.Format("{0}:第{1}个线程", DateTime.Now.ToString(), obj.ToString())); Thread.Sleep(5000); }
执行结果:
由以上的程序中可以看出ThreadPool线程池是一个静态类。线程池可以看做容纳线程的容器;一个应用程序最多只能有一个线程池;ThreadPool静态类通过QueueUserWorkItem()方法将工作函数排入线程池; 每排入一个工作函数,就相当于请求创建一个线程;
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
线程池这样的使用还有一个缺点就是我们无法得知线程在什么时候结束,我们可以使用AutoResetEvent类的WaitOne()方法和Set()方法来获得线程池中线程的执行和返回情况,此方法用于线程同步在此就不详细展开介绍了哈。
最后一个Task,也是我个人比较推荐的,使用方法如下:
static void Main(string[] args) { Console.WriteLine("主线程执行!"); //方法一 Task t1 = new Task(() => { Console.WriteLine("方法1的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法1的任务工作完成……"); }); t1.Start(); //方法二 Task.Run(() => { Console.WriteLine("方法2的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法2的任务工作完成……"); }); //方法三 var t3 = Task.Factory.StartNew(() => { Console.WriteLine("方法3的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法3的任务工作完成……"); }); Console.WriteLine("主线程结束!"); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); }
运行结果:
以上是三种使用Task多线程的方法,Task是基于线程池封装实现的,解决了线程池无法挂起中止线程等这些问题。而且Task的性能优于ThreadPool因为它使用的不是线程池的全局队列,而是使用的是本地队列。使得线程之间竞争资源的情况减少。Task提供了丰富的API,开发者可对Task进行多种管理,控制。对于“Task t1 = new Task(() =>”和“var t3 = Task.Factory.StartNew(() =>”有什么区别,区别并不大后者在调用时是可以传入更多参数,设置线程的运行时间等(关于这一部分的详细介绍可以阅读博文结尾引用的文章C#Task详解)。
带返回值的Task使用方法:
static void Main(string[] args) { Console.WriteLine("主线程执行!"); Task<int> task = CreateTask("Task 1"); task.Start(); int result = task.Result; Console.WriteLine("Task 1 Result is: {0}", result); Console.WriteLine("主线程结束!"); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); } static Task<int> CreateTask(string name) { return new Task<int>(() => TaskMethod(name)); } static int TaskMethod(string name) { Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}", name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); Thread.Sleep(TimeSpan.FromSeconds(2)); return 42; }
运行结果:
以上是带有返回值Task的用法。
接下来介绍以下在Task中常用的一些管理和控制方法:
ContinueWith()方法,在Task线程运行完成后执行,代码如下:
static void Main(string[] args) { Console.WriteLine("主线程执行!"); Task t1 = new Task(() => { Console.WriteLine("方法1的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法1的任务工作完成……"); }); t1.Start(); t1.ContinueWith(t => { Console.WriteLine("方法1的任务工作完成了!"); }); Console.WriteLine("主线程结束!"); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); }
运行结果:
Task.WaitAll(t1, t2);等待t1和t2 Task线程完成,此方法可以传入若干个Tsak线程。会阻塞当前线程,代码如下:
static void Main(string[] args) { Console.WriteLine("主线程执行!"); Task t1 = new Task(() => { Console.WriteLine("方法1的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法1的任务工作完成……"); }); t1.Start(); Task t2 = new Task(() => { Console.WriteLine("方法2的任务开始工作……"); Thread.Sleep(5000); Console.WriteLine("方法2的任务工作完成……"); }); t2.Start(); Task.WaitAll(t1, t2); Console.WriteLine("主线程结束!"); Console.WriteLine("...............按任意键退出"); Console.ReadKey(); }
运行结果:
基于以上的两个方法可以实现线程中的同步和管理等,如果以上的方法不能满足你的开发需要,那需要请你对于某一项单独的类进行更加深入的学习和了解可以浏览以下博客,以下博客均是我在整理和学习这部分知识时有所收获的博客,本文在有些段落和例子也引用于以下博文。
清华大学出版社《C#从入门到精通(第3版)》
浅析C#中的Thread ThreadPool Task和async/await
总结:多线程是一种实现异步的一种方法,在多线程中三个常用的方法,如果是线程要长时间运行的建议使用Thread,如果需要很多线程并发并且线程运行时间较短建议使用ThreadPool,其它的一般情况选择效率相对较高的Task。
以上博文有任何错漏欢迎指正交流。