目录
Task.Run
Task是建立在线程池之上的一种多线程技术,它的出现使Thread成为历史。其使用方法非常简单,下面在顶级语句中做一个简单的例子
void printN(string name) { for (int i = 0; i < 3; i++) { Console.WriteLine($"{name}:{i}"); Task.Delay(1000).Wait(); } } Task.Run(() => printN("t1")); Task.Run(() => printN("t2")); Task.Run(() => printN("t3")).Wait();
运行后,命令行中的结果为
t3:0
t2:0
t1:0
t2:1
t3:1
t1:1
t1:2
t2:2
t3:2
Task.Run通过输入一个委托,创建一个任务并执行,同时返回一个Task对象。
Task在执行过程中,并不会等待命令行的主线程,所以在最后启动的Task后跟上Wait,即等待该线程结束之后,主线程才结束,从而让printN的输出内容得以在终端中显示。
Task类
上面的Task.Run的案例也可以用Task来实现,但Task对象创建后,并不会马上运行,而会在Start()之后运行,示例如下
void printN(object name) { for (int i = 0; i < 3; i++) { Console.WriteLine($"{name}:{i}"); Task.Delay(1000).Wait(); } } Action<object> act = (object name) => printN(name); Task t1 = new Task(()=>printN("t1")); new Task(act, "t2").Start(); Task t3 = new Task(act, "t3"); t1.Start(); t3.Start(); t3.Wait();
返回值
除了Task,C#还提供了带有返回值的封装,即Task<TResult>,可通过泛型的方式声明返回值。
但是,Task说到底还是个类,而非函数,这个返回值并不会在构造函数中体现出来,也不经过Start()函数,若想使用这个返回值,需要经由ContinueWith函数。
ContinueWith的功能是在某一个线程执行完毕之后,开启下一个线程。如果执行完毕的线程有返回值的话,那么ContinueWith也可以利用这个返回值。
其调用方法为
Task<int> task = new Task<int>(() => { Console.WriteLine("这里是task"); return 100; }); //任务完成时执行处理。 Task cwt = task.ContinueWith(t => { Console.WriteLine($"这里是Continue,task返回值为{t.Result}"); }); task.Start(); cwt.Wait();
其中,cwt需要等待task执行完毕之后再执行。
等待和延续
在前面的案例中,已经讲解了基本的等待函数Wait和基本的延续函数ContinueWith。C#中提供了更多的等待与延续函数,以更加灵活地操作线程列表。
阻塞主线程 | 不阻塞主线程 | |
---|---|---|
任意线程执行完毕即可执行 | WaitAny | WhenAny |
所有线程执行完毕方可执行 | WaitAll | WhenAll |
其中, WhenAny, WhenAll需要与ContinueWith配合食用,当WhenXX结束之后,即执行ContinueWith中的内容,这个过程并不阻塞主线程。
为了验证这些函数的功能,先创建一个线程列表
Action<string, int> log = (name, time) => { Console.WriteLine($"{name} Start..."); Task.Delay(time).Wait(); Console.WriteLine($"{name} Stop!"); }; Task[] tasks = new Task[] { Task.Run(() => log("A",3000)), Task.Run(() => log("B",1000)), Task.Run(() => log("C",2000)) };
然后依次执行这几个等待函数,看看结果
Task.WaitAny(tasks); 此时当B执行完毕之后,阻塞就结束了,从而主线程结束。
B Start...
A Start...
C Start...
B Stop!
Task.WaitAll(tasks); 这次当所有线程执行完毕之后,程序才结束。
A Start...
B Start...
C Start...
B Stop!
C Stop!
A Stop!
下面这两组测试,现象和前两组相似,区别无非是在后面加上一段字符串而已。
Task.WhenAny(tasks).ContinueWith(x => Console.WriteLine($"某个Task执行完毕")).Wait(); Task.WhenAll(tasks.ToArray()).ContinueWith(x => Console.WriteLine("所有Task执行完毕")).Wait();
取消任务
C#提供了CancellationToken作为Task取消的标识,通过调用Cancel()函数,可将其取消标志更改为True,从而在线程执行过程中,起到取消线程的作用。
首先创建一个可以取消的线程函数
int TaskMethod(string name, int seconds, CancellationToken token) { Console.WriteLine($"{name} 正在运行"); for (int i = 0; i < seconds; i++) { Task.Delay(1000).Wait(); Console.WriteLine($"{name}: {i}s"); if (token.IsCancellationRequested) return -1; } return 1; }
功能很简单,就是跑循环,在跑循环的过程中,如果token指明取消,则线程结束。
下面测试一下
var cts = new CancellationTokenSource(); var task = new Task<int>(() => TaskMethod("Task 1", 5, cts.Token), cts.Token); Console.WriteLine($"线程状态:{task.Status}"); task.Start(); Console.WriteLine($"线程状态:{task.Status}"); Task.Delay(3000).Wait(); cts.Cancel(); Console.WriteLine($"线程状态:{task.Status}"); Task.Delay(1000).Wait(); Console.WriteLine($"线程状态:{task.Status}");
效果为如下
线程状态:Created
线程状态:WaitingToRun
Task 1 正在运行
Task 1: 0s
Task 1: 1s
线程状态:Running
Task 1: 2s
线程状态:RanToCompletion
在整个线程执行的过程中,共出现了四种状态
- Created 此时线程刚创建,但并未执行
- WaitingToRun 此时已经执行了Start函数,但线程还没反应过来,所以是等待执行
- Running 此时已经执行了Cancel,但task中的循环每1秒检测1次,在Cancel执行完之后,还没来得及检测,就查询了线程的状态,所以线程仍在运行
- RanToCompletion 在等待1秒之后,终于检测到token变了,从而线程结束。