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的时候却有很多疑问. 先来看一段 ...
随机推荐
- ps -eo 用户自定义格式显示
[root@ma ~]# ps -eo pid,ucomm|head -3 PID COMMAND 1 init 2 kthreadd[root@ma ~]# ps axu|head -3USER P ...
- Assuming that agent dropped connection because of access permission
Assuming that agent dropped connection because of access permission
- 【Linux】zabbix4.0服务器搭建,agent搭建,及邮件使用方法
zabbix默认的 服务端监听端口为10051,而被监控端即Zabbix--agents代理程序监控10050端口. 更新yum源: yum clean all yum makecache 需要配置网 ...
- Scrapy———反爬蟲的一些基本應對方法
1. IP地址驗證 背景:有些網站會使用IP地址驗證進行反爬蟲處理,檢查客戶端的IP地址,若同一個IP地址頻繁訪問,則會判斷該客戶端是爬蟲程序. 解決方案: 1. 讓Scrapy不斷隨機更換代理服務器 ...
- SDNU_ACM_ICPC_2021_Winter_Practice_4th [个人赛]
传送门 D - Odd Divisor 题意: 给你一个n,问你n是否至少有一个奇数因子(这里题意没说清,没说是不是只有一个还是可以有多个!AC以后才发现是不止一个 思路: 如果这个数没有奇数因子,那 ...
- 浅析Redis与IO多路复用器原理
为什么Redis使用多路复用I/O Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导 ...
- 学习es6构造函数的第一天
什么是面向对象 编程思维分为,面向过程和面向对象 面向过程就像一个人,一间屋子,一个床 一个人走进了屋子,上了床 二面向对象 人,屋子,床 可以是屋子里进了一个人,上了床 或者,屋子里的床上有一个人 ...
- Trove自动钓鱼脚本(国际服
#WinActivateForce ; Script config. Do NOT change value here, might working inproperly! global Versio ...
- InnoDB 事务隔离级探索
https://mp.weixin.qq.com/s/gWYL2Th9Go5LDhkyGB_rYQ
- list中map 的value值时间排序
public static void main(String[] args) { String sys=DateUtil.getTime().substring(0,5); System.out.pr ...