Task被认为优于线程池
利用拉姆达表达式和本身的泛型支持,可以轻松实现指向常用的方法。
注意:委托(含拉姆达表达式)中调用的方法,其参数如果是变量,它的值取决于运行的那一刻内存里的值。如果希望在创建任务的时候值是固定的,必须用object参数state作为创建任务的状态,把值给进去。
以下两种用法,结果是不同的。
static void Main(string[] args) { Task t; for (int i = 0; i < 10; i++) { //t = new Task(print1,i+1); t = new Task(() => print1(i + 1)); t.Start(); } Console.ReadKey(); } static void print1(object x) { Console.Write($"{x}\t"); }
有一种取巧的做法,可以达到上例的效果,而不用带参创建任务:
1 for (int i = 1; i <= 10; i++) 2 { 3 var j = i; 4 Task.Run(() => print(j)); 5 } 6 Console.ReadKey(); 7 8 static void print(int x) 9 { 10 Console.WriteLine(x); 11 }
循环内反复声明变量j(这种声明方式以前没注意过,但.net6里确实是可以的),并把它作为匿名函数里运行方法的一个参数。
循环内一共创建了10个j,它们在内存中的位置也各不相同。虽然任一时刻只有一个j是有效的,但之前的j里的内容还在。
每次方法在取参数的时候,都会到这次循环的j的内存处去取。取的也就是任务创建时候的j的值了。
**如果没有第四行的语句,则所有的j都在内存的相同位置。这些内容我通过内存观察确认过了,C#的编译器确实想得多,厉害!
Task常规用法为:
1、有参action或func:(//下面两行中的1500和2500都是object类型,t1指向Func,t2指向Action)
var t1 = new Task<int>(add2, 1500);
var t2 = new Task(add1, 2500); t1.Start(); t2.Start();
2、对无参action或func,除了上面的用法,也可以简写为:
var t1 = Task.Run(say); var t2 = Task.Run<int>(say1);
3、某线程结束后可以直接回调
var t = new Task(DoSth); t.Start(); t.ContinueWith(showit, "hi");
如上,后两句可以调换位置。
4、取结果用Task对象的Result属性。会堵塞主线程。
static void Main(string[] args) { DateTime d1 = DateTime.Now; var t1 = Task.Run(say); var t2 = Task.Run<int>(say1); Console.WriteLine(t2.Result); Task.WhenAll(t1,t2).ContinueWith(showtime, d1); Console.WriteLine("hi in main"); Console.ReadKey(); } static void say() { Console.WriteLine("hi in say"); } static int say1() { Thread.Sleep(1000); return 1; } static void showtime(Task t,Object o) { DateTime dt2 = (DateTime)o,dt3=DateTime.Now; var span= dt3- dt2; Console.WriteLine(span.TotalMilliseconds); }
运行结果:
另综合例1:
static void Main(string[] args) { //Task t3 = new Task(() => add1(-25,-29)); //t3.Start(); Task t3 = Task.Run(() => add1(-25, -29));//上两行的立即模式 //Task<int> t4 = new Task<int>(() => add2(66,77)); //t4.Start(); Task<int> t4 = Task.Factory.StartNew(() => add2(66, 77));//上两行的立即模式,不用start //Task<int> t4 = Task.Run<int>(() => add2(66, 77));//同上一行 t3.Wait(1000);//程序等待t3一秒(暂停) //Task[] ta = new Task[] { t3, t4 }; //Task.WaitAll(ta);//同步等待(暂停)无返回 //Task.WhenAll(ta).ContinueWith((t) => Console.WriteLine("lalala"));//异步等待(后续)when会新建一个任务 //Task.WhenAll<int>().... //用法理解:异步,所有任务返回结果,结果组成结果集,被整个方法返回的任务以"result[]"属性的方式呈现。见后面的综合例2 //以下两种,用法与all类似 //Task.WaitAny();//返回整数,表示最先完成的task在数组中的下标。超时为-1 //Task.WhenAny();//返回最先完成的任务 if(t3.IsCompleted) { Console.WriteLine("ok"); } else { Console.WriteLine("no"); } Console.WriteLine(t4.Result); Console.ReadKey(); } static void add1(int x, int y) { Thread.Sleep(2000); Console.WriteLine( x + y); } static int add2(int x,int y) { return x+y; }
另综合例2,多任务计算1+2+3+...+100:
1 class Program 2 { 3 //无论有参无参,action都比func简单。 4 //这里故意把问题复杂化,以做练习~_~ 5 //其实使用action,全局变量累加更容易懂。 6 //计算1+2+...+100 7 static void Main(string[] args) 8 { 9 List<Task<int>> tasks = new List<Task<int>>(); 10 Task<int> t; 11 for (int i = 1; i < 100; i += 10) 12 { 13 t = new Task<int>(add10, new MyData(i)); 14 tasks.Add(t); 15 t.Start(); 16 } 17 Task.WhenAll(tasks.ToArray()).ContinueWith(printsum); 18 Console.WriteLine($"主线程结束"); 19 Console.ReadKey(); 20 } 21 //计算10个数的和 22 static int add10(object x) 23 { 24 int y = ((MyData)x).v,s=0; 25 for (int i = 0; i <= 9; i++) 26 { 27 s += y+i; 28 } 29 return s; 30 } 31 //Task<int[]>是main当中Task.WhenAll(tasks.ToArray())的返回类型 32 //含义:每个任务都返回一个整数,所有任务完成后,就返回一个新任务,它有一个整数数组作为每个任务的结果。 33 //这里也就自然是ContinueWith的参数。(兰姆达表达式的习惯) 34 //打印所有任务的结果之和(即新任务的结果属性数组里的所有元素之和) 35 static void printsum(Task<int[]> ts) 36 { 37 int s = 0; 38 foreach (var item in ts.Result) 39 { 40 s += item; 41 } 42 Console.WriteLine($"Sum is {s}"); 43 } 44 } 45 class MyData 46 { 47 public int v; 48 public MyData(int v1) 49 { 50 v = v1; 51 } 52 }
结果:
关于任务的取消,微软提供了takon(票据、凭据)的方式。
个人觉得1、正常运行的时候,不太有这个需求;2、可以主任务和分任务都能访问到的变量(比如全局变量)来取代它,少一点学习成本。
此处略。