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的时候却有很多疑问. 先来看一段 ...
随机推荐
- Linux 使用命令行上传下载文件
基本语法: 服务器: 用户名@ip:/路径 scp 要拷贝的文件 要存放的文件 上传文件到服务器 # 把本地 source.md 文件上传到 152.116.113.13 服务器的/home目录 # ...
- QPainter 绘制图像接口
阅读本文大概需要 3 分钟 我们在开发软件的过程中,绘制图像功能必不可少,使用 Qt 绘制图像时非常简单,只需要传递几个参数就可以实现功能,在 Qt 中绘制图像的 api有好几个 void drawI ...
- AgileConfig - RESTful API 介绍
AgileConfig AgileConfig是一个基于.net core开发的轻量级配置中心. AgileConfig秉承轻量化的特点,部署简单.配置简单.使用简单.学习简单,它只提取了必要的一些功 ...
- 【ORA】ORA-00030: User session ID does not exist.
今天巡检,查询锁相关的情况的时候,确认业务后,准备将锁干掉,但是干掉的时候报了一个错误,ORA-00030 发现回话不存在,我以为pmon进程已经将锁进程kill掉了,就再次查看,发现,还是存在 这个 ...
- linux中进制转换
方式一:使用$[]或$(()) 格式为:$[base#number]或$((base#number)),其中base为进制,number为对应进制数. 这种方式输入2进制.16进制等,但只能输出为10 ...
- 源代码增强的一点说明(souce code enhance )
souce code enhance 分为显式和隐式两种. 下面以显式创建为例子: 1.在ABAP编辑器中, 打开想要编辑的程序,切换到可编辑模式 2.在源代码中的指定位置右键,弹出菜单,选择 Enh ...
- Java安全之Weblogic 2018-3248分析
Java安全之Weblogic 2018-3248分析 0x00 前言 基于前面的分析,后面的还是主要看补丁的绕过方式,这里就来简单的记录一下. 0x01 补丁分析 先来看看补丁细节 private ...
- 2.4V升5V芯片,8uA功耗,低功耗升压电路图
2.4V升5V,可用于USB拔插充电,也可以用于把两节镍氢电池2.4V升压到5V,的固定输出稳压电压值,同时输出电流可达1A,0.5A等 首先是先说下0.5A的这款的话,是比较低功耗的,8uA左右的输 ...
- ORACLE 归档日志打开关闭方法(转载)
一 设置为归档方式 1 sql> archive log list; #查看是不是归档方式 2 sql> alter system set log_archive_start=true s ...
- scrapy的大文件下载(基于一种形式的管道类实现)
scrapy的大文件下载(基于一种形式的管道类实现) 爬虫类中将解析到的图片地址存储到item,将item提交给指定的管道 在管道文件中导包:from scrapy.pipelines.images ...