聊聊多线程那一些事儿(task)之 一
多线程,一个多么熟悉的词汇,作为一名程序员,我相信无论是从事什么开发语言,都能够轻轻松松说出几种实现多线程的方式,并且在实际工作种也一定用到过多线程,比如:定时器、异步作业等等,如果你说你没有用过多线程,我怀疑你是不是一名程序员,哈哈。
哈哈,言归正传,今天我们要说说c#中的多线线程哪一些事,当然c#在实现多线程上有多种方式,比如:Threads、Action、ThreadPool、Task、Parallel等,当然每一种方式都用其优点和缺点,也有其应用场景,在此不一一说明,今天我们主要以task为例,来一起聊聊task的使用,也就当着一次自我总结提炼罢了。
为什么要用多线程
其实我们在实际使用过程中,使用多线程的目的其实即使为了实现异步+并行,异步:是相对同步的,同步就是一个流程安装一个流程执行完毕,异步就是在不影响主流程的执行同时,可以执行其他流程,这也就是达到了几个逻辑并行执行的效果。当然了,不是说异步就完全是独立执行,相互间就没有关联关系,其实在异步的同时,也可以在特定节点等待阻塞等待异步结果啦。说了半天废话,不要走开,主题才刚刚开始,下面以实际例子来演绎task的实际使用吧!
如何创建和运行一个task
微软就是那么6逼,在创建和执行一个task时,都给大家提供了多种方式来实现,大家可以根据其具体的使用场景和习惯来选择最适合的方式,下面通过代码来简单说明如下的三种实现方式:
/// <summary> /// 简单的task创建方式演示 /// </summary> private static void TaskCreatFun() { // 其一、通过传统的 new 方式来实例化一个task对象,这种方式需要手动通过start来启动 Task newTask = new Task(() => { Thread.Sleep(1000); Console.WriteLine($"Hello Engineer, 我是 new 的一个task,线程ID:Thread.CurrentThread.ManagedThreadId}"); }); // 启动 tsak newTask.Start(); // 其二、通过工厂 factory 来生成一个task对象,并自启动 Task factoryTask = Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine($"Hello Engineer, 我是 factory 生产 的一个task,线程ID:Thread.CurrentThread.ManagedThreadId}"); }); // 其三、通过 Task.Run(Action action) 来创建一个自启动task Task runTask = Task.Run(() => { Thread.Sleep(1000); Console.WriteLine($"Hello Engineer, 我是 Task.Run 创建一个自启动task,线程ID:Thread.CurrentThread.ManagedThreadId}"); }); runTask.RunSynchronously(); Console.WriteLine($"Hello Engineer, 我是主线程啦!线程ID{Thread.CurrentThread.ManagedThreadId}"); }
代码的执行结果:很容易看出,task执行阻塞主线程,并且几个task并行执行。
如何创建一个带有返回值的task
在上面的代码实例种,我们以不同的方式创建并运行了一个多线程,但是这几个task都是没有返回值的,这样的task适用于:独立执行的一个子业务,完全不关心其执行结果。但是我们在实际使中,往往会需要关系其执行结果的。
以一个实际的业务场景来说明:比如,我们在一个酒店预订系统中,需要实时到不同的第三接口实时查询某一个酒店的某一客房在最新状态,比如有3个接口渠道:携程、艺龙、去哪儿,该如何实现呢?
首先,如果我们采用串行的方式一个一个的去取数据,那样估计你的系统慢到不会有人用的,所以我们第一个想到的是,采用task来并行获取,并返回获取到的结果值,下面简单模拟一下代码实现:
/// <summary> /// 获取最新的客房信息 /// </summary> /// <returns>客房信息集合</returns> private static List<string> GetHotelRoomInfro() { // 模拟存储获取到的酒店客房数据集合 List<string> listHotelRoomInfro = new List<string>(); Console.WriteLine("下面通过3个task,并行的到不同接口方获取实时的客房信息:"); Console.WriteLine(""); // 在此我也分别对3种不同渠道,采用3种不同的方式来实现 // 其一、通过传统的 new 方式来实例化一个task对象,获取 携程 的客房数据 Task<string> newCtripTask = new Task<string>(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); Console.WriteLine("携程 内部处理完毕!"); return "我是来自 携程 的最新客房信息"; }); // 启动 tsak newCtripTask.Start(); // 其二、通过工厂 factory 来生成一个task对象,并自启动:获取 艺龙 的客房数据 Task<string> factoryElongTask = Task<string>.Factory.StartNew(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); Console.WriteLine("艺龙 内部处理完毕!"); return "我是来自 艺龙 的最新客房信息"; }); // 其三、通过 Task.Run(Action action) 来创建一个自启动task:获取 去哪儿网 的客房数据 Task<string> runQunarTask = Task<string>.Run(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); Console.WriteLine("去哪儿网 内部处理完毕!"); return "我是来自 去哪儿网 的最新客房信息"; }); // 分别打印不同渠道的客房数据 Console.WriteLine(newCtripTask.Result); Console.WriteLine(factoryElongTask.Result); Console.WriteLine(runQunarTask.Result); Console.WriteLine(""); Console.WriteLine("所有接口方的最新客房数据获取完毕!"); return listHotelRoomInfro; }
执行结果:
其实通过上面的执行结果,我们不难看出以下几点
1、task虽然是异步线程,但是可以有返回值的
2、task的返回值获取方式为:task.Result
3、在通过task.Result获取返回值时,会阻塞主线程,其实也不难理解,你要等待处理结果,肯定会阻塞等待啦!
task可以同步执行吗?
通过上面的实际代码测试,我们知道task都是异步执行,那么有人会问,task可以实现同步执行吗?不急,强大的微软也想到了这个问题,于是乎,提供了 task.RunSynchronously() 来同步执行,但是这种方式执行,只有通过new 实例化的task才有效,原因也很简单,其他两种方式创建task都已经自启动执行了,不可能在来一个同步启动执行吧,嘿嘿。下面我们用代码来演示:
/// <summary> /// 通过RunSynchronously 实现task的同步执行 /// </summary> private static void TaskRunSynchronously() { Console.WriteLine("主线程开始执行!"); Task<string> task = new Task<string>(() => { Thread.Sleep(100); Console.WriteLine("Task执行结束!"); return ""; }); /// task.Start(); /// task.Wait(); // 获取执行结果,会阻塞主流程 // string result = task.Result; //同步执行,task会阻塞主线程 task.RunSynchronously(); Console.WriteLine("执行主线程结束!"); Console.ReadKey(); }
执行结果:很明显主线程阻塞等待task同步执行。
task同步执行,出了上面的实现方式,其实我们也可以通过task.wait()来变相的实现同步执行效果,当然也可以用task.Result来变现的实现,原理很简单,因为wait()和Result都是要阻塞主流程,直到task执行完毕,是不是有异曲同工之妙呢!以代码为例:
通过task.wait()实现,只需要对上面的代码做一个简单的调整,如下:其最终的效果一样:
/// <summary> /// 通过RunSynchronously 实现task的同步执行 /// </summary> private static void TaskRunSynchronously() { Console.WriteLine("主线程开始执行!"); Task<string> task = new Task<string>(() => { Thread.Sleep(100); Console.WriteLine("Task执行结束!"); return ""; }); task.Start(); task.Wait(); // 获取执行结果,会阻塞主流程 // string result = task.Result; // 同步执行,task会阻塞主线程 // task.RunSynchronously(); Console.WriteLine("执行主线程结束!"); Console.ReadKey(); }
执行结果:
通过task.Result 实现,前提是task一定要有返回值,如下:其最终的效果一样:
/// <summary> /// 通过RunSynchronously 实现task的同步执行 /// </summary> private static void TaskRunSynchronously() { Console.WriteLine("主线程开始执行!"); Task<string> task = new Task<string>(() => { Thread.Sleep(100); Console.WriteLine("Task执行结束!"); return ""; }); task.Start(); /// task.Wait(); // 获取执行结果,会阻塞主流程 string result = task.Result; // 同步执行,task会阻塞主线程 // task.RunSynchronously(); Console.WriteLine("执行主线程结束!"); Console.ReadKey(); }
执行效果也和上面的两种方式一样。
当然我还可以通过task.IsCompleted来变现实现,在此就不在细说,简单一个代码示意即可:while (!task.IsCompleted){}
当然我上面说的几种实现同步的方式,只是为了拓展一下思路,不一定都是最优方案。
Task的Wait、WaitAny、WaitAll方法介绍
task的基本创建和用法,上面都做了简单的介绍,但是在我们实际业务场景中,往往不是那么简单的单纯实现。比如:还是刚刚上面的那个酒店信息获取为例,现在新的需求是:3个渠道的接口实时数据,我们只需要获取到其中的一个就立即返回会用户,避免用户等待太久,那么这个时候task.WaitAny就派上用场了,WaitAny就是等待一组tsak集合中,只要有一个执行完毕就不在等待,与之对应的是WaitAll需要等待一组tsak集合中所有tsak都执行完毕,当然了Wait是针对一个task的,等待本身执行完成,上面的模拟同步执行已经说了,就不在啰嗦。
/// <summary> /// 获取最新的客房信息(只需要获取到一个即可) /// </summary> /// <returns>客房信息集合</returns> private static List<string> GetOneHotelRoomInfro() { // 模拟存储获取到的酒店客房数据集合 List<string> listHotelRoomInfro = new List<string>(); Console.WriteLine("下面通过3个task,并行的到不同接口方获取实时的客房信息:"); Console.WriteLine(""); // 在此我也分别对3种不同渠道,采用3种不同的方式来实现 // 其一、通过传统的 new 方式来实例化一个task对象,获取 携程 的客房数据 Task newCtripTask = new Task(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); listHotelRoomInfro.Add("我是来自 携程 的最新客房信息"); }); // 启动 tsak newCtripTask.Start(); // 其二、通过工厂 factory 来生成一个task对象,并自启动:获取 艺龙 的客房数据 Task factoryElongTask = Task.Factory.StartNew(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); listHotelRoomInfro.Add("我是来自 艺龙 的最新客房信息"); }); // 其三、通过 Task.Run(Action action) 来创建一个自启动task:获取 去哪儿网 的客房数据 Task runQunarTask = Task.Run(() => { // 具体获取业务逻辑处理... Thread.Sleep(new Random().Next(100, 1000)); listHotelRoomInfro.Add("我是来自 去哪儿网 的最新客房信息"); }); // 只需要等待一个有返回即可 Task.WaitAny(new Task[] { newCtripTask, factoryElongTask, runQunarTask }); // 等待所有接口数据返回 // Task.WaitAll(new Task[] { newCtripTask, factoryElongTask, runQunarTask }); Console.WriteLine("已经有接口数据返回!"); foreach (var item in listHotelRoomInfro) { Console.WriteLine($"返回的接口数据为:{item}"); } Console.WriteLine("主线程执行完毕!"); Console.ReadKey(); Console.WriteLine(""); return listHotelRoomInfro; }
上面是演示了WaitAny的执行结果,至于WaitAll就不在演示了,一看便知
好了,今天时间差不多了,就介绍到儿了,明天我们在来研究:
1、task的延续操作(WhenAny/WhenAll/ContinueWith)
2、任务取消(CancellationTokenSource)
3、实际案例分析