C#异步和多线程以及Thread、ThreadPool、Task区别和使用方法
本文的目的是为了让大家了解什么是异步?什么是多线程?如何实现多线程?对于当前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。
以上博文有任何错漏欢迎指正交流。
C#异步和多线程以及Thread、ThreadPool、Task区别和使用方法的更多相关文章
- c#中@标志的作用 C#通过序列化实现深表复制 细说并发编程-TPL 大数据量下DataTable To List效率对比 【转载】C#工具类:实现文件操作File的工具类 异步多线程 Async .net 多线程 Thread ThreadPool Task .Net 反射学习
c#中@标志的作用 参考微软官方文档-特殊字符@,地址 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/toke ...
- 从Thread,ThreadPool,Task, 到async await 的基本使用方法解读
记得很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题 ...
- Thread,ThreadPool,Task, 到async await 的基本使用方法和理解
很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题吧. ...
- 异步多线程 Thread ThreadPool Task
一.线程 Thread ThreadPool 线程是Windows任务调度的最小单位,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针.程序计数器等),但代码区是共享的,即不同的线程可以 ...
- .NET多线程(Thread,ThreadPool,Task,Async与Await)
.NET多线程是什么? 进程与线程 进程是一种正在执行的程序. 线程是程序中的一个执行流. 多线程是指一个程序中可以同时运行多个不同的线程来执行不同的任务. .NET中的线程 Thread是创建和控制 ...
- .net 多线程 Thread ThreadPool Task
先准备一个耗时方法 /// <summary>/// 耗时方法/// </summary>/// <param name="name">< ...
- Thread,ThreadPool,Task
线程分为前台和后台.比如我们直接new一个Thread这就是前台线程. 前台线程一定会执行. 比如我们创建2个线程:1号,2号,同时执行,假设1号是主线程,1执行完了,依旧会等待2执行完成,整个程序才 ...
- 异步和多线程,委托异步调用,Thread,ThreadPool,Task,Parallel,CancellationTokenSource
1 进程-线程-多线程,同步和异步2 异步使用和回调3 异步参数4 异步等待5 异步返回值 5 多线程的特点:不卡主线程.速度快.无序性7 thread:线程等待,回调,前台线程/后台线程, 8 th ...
- 浅析C#中的Thread ThreadPool Task和async/await
.net 项目中不可避免地要与线程打交道,目的都是实现异步.并发.从最开始的new Thread()入门,到后来的Task.Run(),如今在使用async/await的时候却有很多疑问. 先来看一段 ...
随机推荐
- pyi文件是干嘛的?(一文读懂Python的存根文件和类型检查)
参考资料: https://blog.csdn.net/weixin_40908748/article/details/106252884 https://www.python.org/dev/pep ...
- SAP中使用FTP服务
SAP中简单的FTP技术实现基本上如下几个步骤: 1.SM59建立FTP的RFC destination. 可以通过执行SAP的标准程序RSFTP005,自动创建两个名为SAPFTP何SAPFTPA的 ...
- ASP.NET Core错误处理中间件[4]: 响应状态码页面
StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处 ...
- ElasticSearch极简入门总结
一,目录 安装es 项目添加maven依赖 es客户端组件注入到spring容器中 es与mysql表结构对比 索引的删除创建 文档的crud es能快速搜索的核心-倒排索引 基于倒排索引的精确搜索. ...
- 微服务网关2-搭建Gateway服务
一.创建父模块infrastructure 1.创建模块 在guli_parent下创建普通maven模块 Artifact:infrastructure 2.删除src目录 二.创建模块api_ga ...
- 微信小程序 发送模板消息的功能实现
背景 - 小程序开发的过程中,绝大多数会满足微信支付 - 那么,作为友好交互的体现,自然就会考虑到支付后的消息通知咯 - 所以,我的小程序项目也要求完成这个效果,so.分享一下自己的实现步骤,以方便道 ...
- 前端面试之JavaScript中数组的方法!【残缺版!!】
前端面试之JavaScript中数组常用的方法 7 join Array.join()方法将数组中所有元素都转化为字符串并连接在-起,返回最后生成的字 符串.可以指定一个可选的字符串在生成的字符串中来 ...
- 使用cacti监控linux主机
介绍:使用cacti监控linux主机,需要在linux主机上面安装snmp服务,并修改snmpd.conf文件,指定cacti服务器的地址,然后在cacti的前台界面添加此主机即可,此处以监控cen ...
- Linux安装redis报错:jemalloc/jemalloc.h: No such file or directory踩坑
报错内容: 针对这个错误,我们可以在README.md 文件中看到解释: --------- Selecting a non-default memory allocator when buildin ...
- 内存模型 Memory model 内存分布及程序运行中(BSS段、数据段、代码段、堆栈
C语言中内存分布及程序运行中(BSS段.数据段.代码段.堆栈) - 秦宝艳的个人页面 - 开源中国 https://my.oschina.net/pollybl1255/blog/140323 Mem ...