第九节:深究并行编程Parallel类中的三大方法 (For、ForEach、Invoke)和几大编程模型(SPM、APM、EAP、TAP) 第八节:Task的各类Task<TResult>返回值以及通用线程的异常处理方案。 第七节:利用CancellationTokenSource实现任务取消和利用CancellationToken类检测取消异常。 第六节:深入研究Task实例方法Cont
第九节:深究并行编程Parallel类中的三大方法 (For、ForEach、Invoke)和几大编程模型(SPM、APM、EAP、TAP)
一. 并行编程
1. 区分串行编程和串行编程
①. 串行编程:所谓的串行编程就是单线程的作用下,按顺序执行。(典型代表for循环 下面例子从1-100按顺序执行)
②. 并行编程:充分利用多核cpu的优势,同时开启多个线程并行执行。(典型代表Parallel.For循环 下面例子从1-100无序执行)
代码实践:
1 { 2 //1. 串行 (从1-100按顺序执行) 3 for (int i = 1; i < 100; i++) 4 { 5 Console.WriteLine(i); 6 } 7 //2. 并行 (从1-100无序执行) 8 Parallel.For(1, 100, (item) => 9 { 10 Console.WriteLine(item); 11 }); 12 }
结论:串行的代码按顺序依次输出,并行的代码无顺序输出。
2. 深究Parallel类中的方法 (For方法、ForEach方法、Invoke方法 这三个方法都是用来开启线程的)
(1). Invoke方法
a. 该方法的作用就是用来同时开启多个线程的。
b. 该方法有两个重载,主要涉及到两个参数,用来配置最大并行数(即线程数)和一个可变的Action委托数组(详见源码)。
案例一: 开启五个不同的线程调用五个方法
我们发现一个现象,主线程等着这五个子线程执行完毕后才执行,但是我们并没有写线程等待的代码,所以我们可以总结:
①:并行计算,开启多个线程后,不需要再开辟线程等待,直接是主线程完成后续操作。
②:而普通多线程执行后,需要单独再开辟一个线程等待,然后主线程在执行。
代码实践:
1 { 2 Parallel.Invoke(() => this.TestThread("bct1") 3 , () => this.TestThread("bct2") 4 , () => this.TestThread("bct3") 5 , () => this.TestThread("bct4") 6 , () => this.TestThread("bct5") 7 ); 8 }
案例二: 指定最大并行数进行线程调用
我们发现,五个任务中的四个任务同时由不同线程开启,当其中一个任务结束时,第五个任务开启,并由刚结束的任务的线程来执行。
{ //设置最大的线程并行数 ParallelOptions p = new ParallelOptions(); p.MaxDegreeOfParallelism = 4; Parallel.Invoke(p, () => this.TestThread("bct1") , () => this.TestThread("bct2") , () => this.TestThread("bct3") , () => this.TestThread("bct4") , () => this.TestThread("bct5") ); }
(2). For方法 (前两个参数之间的差代表任务的个数)
这里介绍一个简单重载: public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body);
fromInclusive:开始索引(含).
toExclusive:结束索引(不含).
body:将为每个迭代调用一次的委托.
当然该方法中的其他重载中也有很丰富的功能,比如也可以配置最大线程数。
代码实践:
1 { 2 //案例一:前两个参数之间的差,就为并行计算线程的个数 3 { 4 Parallel.For(5, 10, t => 5 { 6 //这里的t分别为:5,6,7,8,9 五个数 7 string name = string.Format("bct{0}", t); 8 this.TestThread(name); 9 }); 10 } 11 //案例二: 配置最大并行数 12 //结果:同时最多5个线程执行,但是还是要执行9个任务,(6,7,8,9,10,11,12,13,14),后面四个任务等前面的执行完后,再执行 13 { 14 ParallelOptions po = new ParallelOptions() 15 { 16 MaxDegreeOfParallelism = 5 //表示最大线程数为5,后面即使配置超过5,也无效 17 }; 18 Parallel.For(6, 15, po, (t, state) => 19 { 20 string name = string.Format("bct{0}", t); 21 this.TestThread(name); 22 //state.Break(); //退出单次循环(没看到实际作用) 23 // state.Stop(); //退出全部循环(没看到实际作用) 24 //return; 25 }); 26 } 27 }
(3). ForEach方法
这里也是介绍一个简单的重载:int数组中的个数代表需要进行并行任务的个数,但并不一定所有任务同时执行,也不一定每个任务都是一个新线程执行。
该方法当然也可以配置最大并行数。
代码实践:
{ //数组里的个数,就为并行进行并行任务数 Parallel.ForEach(new int[] { 3, 5, 44, 55, 100 }, t => { //这里的t分别为:3, 5, 44, 55, 100五个数 string name = string.Format("bct{0}", t); this.TestThread(name); } }
二. 常见的编程模型
1.同步编程模型(SPM):单线线程、串行开发模式。
2.异步编程模型(APM):xxxbegin、xxxend的模式。
3.基于事件的编程模型(EAP): xxAsync这样的事件模式。 eg:WebClient。
4.基于Task的编程模型(TAP): APM和EAP都可以使用Task来实现,微软的初衷就是想通过Task大一统异步编程领域。
下面分享两段代码,不做深入研究了。
1 { 2 FileStream fs = new FileStream(Environment.CurrentDirectory + "//1.txt", FileMode.Open); 3 var bytes = new byte[fs.Length]; 4 var task = Task.Factory.FromAsync(fs.BeginRead, fs.EndRead, bytes, 0, bytes.Length, string.Empty); 5 6 var nums = task.Result; 7 8 Console.WriteLine(nums); 9 } 10 { 11 FileStream fs = new FileStream(Environment.CurrentDirectory + "//1.txt", FileMode.Open); 12 13 var bytes = new byte[fs.Length]; 14 15 fs.BeginRead(bytes, 0, bytes.Length, (aysc) => 16 { 17 var nums = fs.EndRead(aysc); 18 19 Console.WriteLine(nums); 20 21 }, string.Empty); 22 23 Console.Read(); 24 }
第八节:Task的各类Task<TResult>返回值以及通用线程的异常处理方案。
一. Task的各种返回值-Task<TResult>
PS: 在前面章节,我们介绍了Task类开启线程、线程等待、线程延续的方式,但我们并没有关注这些方式的返回值,其实他们都是有返回值的Task<TResult>,然后可以通过Task的实例调用Result属性来获取这个返回值。
下面我们分三类来介绍:
①:线程开启类的返回值, 使用Task<TResult>接受,或者直接使用Task接受,通过 实例.Result 来获取返回值。这里的线程开启类有多种,eg: Task.Run()、 task.start()、 Task.Factory.StartNew() 等。
②:线程延续类的返回值. eg:ContinueWith。
③:线程条件延续类的返回值. eg:WhenAll和WhenAny。
1. 线程开启类的返回值
1 { 2 Task<string> task1 = Task.Factory.StartNew(() => 3 { 4 Console.WriteLine("我是子线程哦"); 5 return "ok"; 6 }); 7 task1.Wait(); 8 Console.WriteLine("我是主线程,我要读取子线程task1的返回值为:{0}", task1.Result); 9 }
2. 线程延续类的返回值
1 { 2 Task<int> task1 = Task.Run(() => 3 { 4 Console.WriteLine("我是子线程1哦"); 5 return 2; 6 }); 7 8 var task2 = task1.ContinueWith((t) => 9 { 10 Console.WriteLine("我是子线程2哦"); 11 12 //这里的t代表 task1 13 var num = t.Result + 2; 14 return num.ToString(); 15 }); 16 17 task2.Wait(); 18 Console.WriteLine("我是主线程,我要读取子线程task1的返回值为:{0}", task1.Result); 19 Console.WriteLine("我是主线程,我要读取子线程task2的返回值为:{0}", task2.Result); 20 }
3. 线程条件延续类
1 { 2 Task<int> task1 = Task.Run(() => 3 { 4 Console.WriteLine("我是子线程1哦"); 5 return 1; 6 }); 7 Task<int> task2 = Task.Run(() => 8 { 9 Console.WriteLine("我是子线程2哦"); 10 return 2; 11 }); 12 13 var task = Task.WhenAny(new Task<int>[2] { task1, task2 }); 14 task.Wait(); 15 16 //下面的值可能是1,也可能是2 17 Console.WriteLine("我是主线程,我要读取子线程task的返回值为:{0}", task.Result.Result); 18 }
二. 通用线程异常处理方案
1. 背景:我们想达到一个目的,当同时开启多个线程的时候,其中一个线程报错,不影响其他线程的执行,并且能把错误记下来。
2. 解决方案:多重try-catch,整个外侧主线程一个try-catch,然后线程执行业务再用一个try-catch包裹起来。
常规方式捕获异常:
1 { 2 try 3 { 4 for (int i = 0; i < 5; i++) 5 { 6 string name = string.Format("name{0}", i); 7 var task = Task.Run(() => 8 { 9 try 10 { 11 //模拟某个线程出错 12 if (name == "name2") 13 { 14 throw new Exception(string.Format("线程执行失败,i={0}", name)); 15 } 16 else 17 { 18 Console.WriteLine(string.Format("线程执行执行成功,i={0}", name)); 19 } 20 } 21 catch (Exception ex) 22 { 23 Console.WriteLine(ex.Message); 24 } 25 26 }); 27 taskList.Add(task); 28 } 29 Task.WaitAll(taskList.ToArray()); 30 } 31 catch (Exception ex) 32 { 33 Console.WriteLine(ex.Message); 34 35 } 36 }
运行结果:我们发现所有的线程均执行完毕,且name2执行失败,并捕获。
补充一下:通过 AggregateException 类来捕获异常。
1 { 2 try 3 { 4 for (int i = 0; i < 5; i++) 5 { 6 string name = string.Format("name{0}", i); 7 var task = Task.Run(() => 8 { 9 throw new Exception(string.Format("线程执行失败,i={0}", name)); 10 }); 11 taskList.Add(task); 12 } 13 Task.WaitAll(taskList.ToArray()); 14 } 15 catch (AggregateException aes) 16 { 17 foreach (var item in aes.InnerExceptions) 18 { 19 Console.WriteLine(item.Message); 20 } 21 } 22 }
第七节:利用CancellationTokenSource实现任务取消和利用CancellationToken类检测取消异常。
一. 传统的线程取消
所谓的线程取消,就是线程正在执行的过程中取消线程任务。
传统的线程取消,是通过一个变量来控制,但是这种方式,在release模式下,被优化从cpu高速缓存中读取,而不是从内存中读取,会造成主线程无法执行这一个bug。
1 { 2 var isStop = false; 3 var thread = new Thread(() => 4 { 5 while (!isStop) 6 { 7 Thread.Sleep(100); 8 Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); 9 } 10 }); 11 thread.Start(); 12 Thread.Sleep(1000); 13 isStop = true; 14 }
PS: 通过上面的代码看可以看出来,传统模式的线程取消,在排除release模式bug的情况下,局限性还是很明显的。比如:当子线程任务取消的那一刻,我想执行另外一项任务;我想延时取消一个线程任务;线程取消的时候抛异常。
上述这几种情况,我们都要借助单独的类来处理。
二. CancellationTokenSource实现任务取消
1. 取消任务的同时触发一个函数
利用Cancel方法、Register注册、source.Token标记取消位来实现。
{ CancellationTokenSource source = new CancellationTokenSource(); //注册一个线程取消后执行的逻辑 source.Token.Register(() => { //这里执行线程被取消后的业务逻辑. Console.WriteLine("-------------我是线程被取消后的业务逻辑---------------------"); }); Task.Run(() => { while (!source.IsCancellationRequested) { Thread.Sleep(100); Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); } }, source.Token); Thread.Sleep(2000); source.Cancel(); }
2. 延时取消
线程的延时取消有两种方式:
方案一:CancelAfter方法。
1 #region 方案一:CancelAfter方法 2 { 3 CancellationTokenSource source = new CancellationTokenSource(); 4 //注册一个线程取消后执行的逻辑 5 source.Token.Register(() => 6 { 7 //这里执行线程被取消后的业务逻辑. 8 Console.WriteLine("-------------我是线程被取消后的业务逻辑---------------------"); 9 }); 10 11 Task.Run(() => 12 { 13 while (!source.IsCancellationRequested) 14 { 15 Thread.Sleep(100); 16 Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); 17 } 18 }, source.Token); 19 20 Thread.Sleep(2000); 21 //4s后自动取消 22 source.CancelAfter(new TimeSpan(0, 0, 0, 4)); 23 } 24 #endregion
方案二:CancellationTokenSource构造函数(不再需要Cancel方法了)。
1 { 2 //4s后自动取消 3 CancellationTokenSource source = new CancellationTokenSource(4000); 4 //注册一个线程取消后执行的逻辑 5 source.Token.Register(() => 6 { 7 //这里执行线程被取消后的业务逻辑. 8 Console.WriteLine("-------------我是线程被取消后的业务逻辑---------------------"); 9 }); 10 11 Task.Run(() => 12 { 13 while (!source.IsCancellationRequested) 14 { 15 Thread.Sleep(100); 16 Console.WriteLine("当前thread={0} 正在运行", Thread.CurrentThread.ManagedThreadId); 17 } 18 }, source.Token); 19 20 Thread.Sleep(2000); 21 }
3. 组合取消
利用CreateLinkedTokenSource构建CancellationTokenSource的组合体,其中任何一个体取消,则组合体就取消。
{ CancellationTokenSource source1 = new CancellationTokenSource(); //source1.Cancel(); CancellationTokenSource source2 = new CancellationTokenSource(); source2.Cancel(); var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token); Console.WriteLine("s1={0} s2={1} s3={2}", source1.IsCancellationRequested, source2.IsCancellationRequested, combineSource.IsCancellationRequested); }
上述代码,source1和source2中的任何一个取消,combineSource就会被取消。
三. CancellationToken类监控取消
CancellationToken类下ThrowIfCancellationRequested属性,等价于if (XXX.IsCancellationRequested){throw new Exception("报错了");}
只要取消就报错。
1 { 2 CancellationTokenSource source1 = new CancellationTokenSource(); 3 CancellationTokenSource source2 = new CancellationTokenSource(); 4 var combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token); 5 source1.Cancel(); 6 7 //if (combineSource.IsCancellationRequested) 8 //{ 9 // throw new Exception("报错了"); 10 //} 11 12 //等价于上面那句话 13 try 14 { 15 combineSource.Token.ThrowIfCancellationRequested(); 16 } 17 catch (Exception) 18 { 19 Console.WriteLine("报错了"); 20 } 21 22 23 Console.WriteLine("s1={0} s2={1} s3={2}", source1.IsCancellationRequested, 24 source2.IsCancellationRequested, 25 combineSource.IsCancellationRequested); 26 }
第六节:深入研究Task实例方法ContinueWith的参数TaskContinuationOptions
一. 整体说明
揭秘:
该章节的性质和上一个章节类似,也是一个扩展的章节,主要来研究Task类下的实例方法ContinueWith中的参数TaskContinuationOptions。
通过F12查看TaskContinuationOptions的源码,知道主要有这么几个参数:
①. LazyCancellation:在延续取消的情况下,防止延续的完成直到完成先前的任务。
(下面的例子task2取消,原先的延续关系不复存在,task1和task3可以并行执行)
②. ExecuteSynchronously:希望执行前面那个task的thread也在执行本延续任务
(下面的例子执行task2的Thread和执行task1的Thread是同一个,所有二者的线程id相同)
③. NotOnRanToCompletion和OnlyOnRanToCompletion
NotOnRanToCompletion:延续任务必须在前面task非完成状态才能执行
OnlyOnRanToCompletion:延续任务必须在前面task完成状态才能执行
(下面例子:注释掉异常的这句代码task2不能执行,task3能执行;不注释,task2能执行,task3不能执行)
源码如下:
1 [Serializable] 2 [Flags] 3 public enum TaskContinuationOptions 4 { 5 // 摘要: 6 // Default = "Continue on any, no task options, run asynchronously" 指定应使用默认行为。 7 // 默认情况下,完成前面的任务之后将安排运行延续任务,而不考虑前面任务的最终 System.Threading.Tasks.TaskStatus。 8 None = 0, 9 // 10 // 摘要: 11 // 提示 System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。 12 PreferFairness = 1, 13 // 14 // 摘要: 15 // 指定某个任务将是运行时间长、粗粒度的操作。 它会向 System.Threading.Tasks.TaskScheduler 提示,过度订阅可能是合理的。 16 LongRunning = 2, 17 // 18 // 摘要: 19 // 指定将任务附加到任务层次结构中的某个父级。 20 AttachedToParent = 4, 21 // 22 // 摘要: 23 // 如果尝试附有子任务到创建的任务,指定 System.InvalidOperationException 将被引发。 24 DenyChildAttach = 8, 25 // 26 // 摘要: 27 // 防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default 28 // 当前计划程序。 29 HideScheduler = 16, 30 // 31 // 摘要: 32 // 在延续取消的情况下,防止延续的完成直到完成先前的任务。 33 LazyCancellation = 32, 34 // 35 // 摘要: 36 // 指定不应在延续任务前面的任务已完成运行的情况下安排延续任务。 此选项对多任务延续无效。 37 NotOnRanToCompletion = 65536, 38 // 39 // 摘要: 40 // 指定不应在延续任务前面的任务引发了未处理异常的情况下安排延续任务。 此选项对多任务延续无效。 41 NotOnFaulted = 131072, 42 // 43 // 摘要: 44 // 指定只应在延续任务前面的任务已取消的情况下才安排延续任务。 此选项对多任务延续无效。 45 OnlyOnCanceled = 196608, 46 // 47 // 摘要: 48 // 指定不应在延续任务前面的任务已取消的情况下安排延续任务。 此选项对多任务延续无效。 49 NotOnCanceled = 262144, 50 // 51 // 摘要: 52 // 指定只应在延续任务前面的任务引发了未处理异常的情况下才安排延续任务。 此选项对多任务延续无效。 53 OnlyOnFaulted = 327680, 54 // 55 // 摘要: 56 // 指定只应在延续任务前面的任务已完成运行的情况下才安排延续任务。 此选项对多任务延续无效。 57 OnlyOnRanToCompletion = 393216, 58 // 59 // 摘要: 60 // 指定应同步执行延续任务。 指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。 如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。 61 // 只应同步执行运行时间非常短的延续任务。 62 ExecuteSynchronously = 524288, 63 }
二. 实际测试
下面通过代码来说明默认情况下、LazyCancellation、ExecuteSynchronously、NotOnRanToCompletion和OnlyOnRanToCompletion的作用和效果。
1. 默认情况
默认情况下,task1执行完后→task2→task2执行完后→task3。
1 { 2 Task task1 = new Task(() => 3 { 4 Thread.Sleep(1000); 5 Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 6 }); 7 8 var task2 = task1.ContinueWith(t => 9 { 10 Console.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 11 }); 12 13 var task3 = task2.ContinueWith(t => 14 { 15 Console.WriteLine("task3 tid={0}, dt={1} {2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, task2.Status); 16 }); 17 18 task1.Start(); 19 20 }
运行结果: task1执行完后→ task2执行→task2执行完后→ task3执行。
2. LazyCancellation
作用:取消该线程,该线程的前一个线程和后一个线程并行执行。
1 { 2 CancellationTokenSource source = new CancellationTokenSource(); 3 source.Cancel(); 4 5 Task task1 = new Task(() => 6 { 7 Thread.Sleep(1000); 8 Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 9 }); 10 11 var task2 = task1.ContinueWith(t => 12 { 13 Console.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 14 }, source.Token, TaskContinuationOptions.LazyCancellation, TaskScheduler.Current); 15 16 var task3 = task2.ContinueWith(t => 17 { 18 Console.WriteLine("task3 tid={0}, dt={1} {2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, task2.Status); 19 }); 20 21 task1.Start(); 22 23 }
运行结果: task2线程已经被取消,task1线程和task2线程并行执行。
3. ExecuteSynchronously
作用:希望执行前面那个task的thread也在执行本延续任务。
1 { 2 Task task1 = new Task(() => 3 { 4 Thread.Sleep(1000); 5 Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 6 }); 7 8 var task2 = task1.ContinueWith(t => 9 { 10 Console.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); 11 }, TaskContinuationOptions.ExecuteSynchronously); 12 13 task1.Start(); 14 }
结果:task1和task2线程的线程id相同。
4. NotOnRanToCompletion和OnlyOnRanToCompletion
NotOnRanToCompletion:延续任务必须在前面task非完成状态才能执行。
OnlyOnRanToCompletion:延续任务必须在前面task完成状态才能执行。
{ Task task1 = new Task(() => { Thread.Sleep(1000); Console.WriteLine("task1 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); //手动制造异常,表示不能执行完毕 //(注释掉这句话task2不能执行,task3能执行) //不注释,task2能执行,task3不能执行 //throw new Exception("hello world"); }); var task2 = task1.ContinueWith(t => { Console.WriteLine("task2 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); }, TaskContinuationOptions.NotOnRanToCompletion); var task3 = task1.ContinueWith(t => { Console.WriteLine("task3 tid={0}, dt={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now); }, TaskContinuationOptions.OnlyOnRanToCompletion); task1.Start(); }
分析:task2和task3均为task的延续线程,当task1报错时候,task2执行,task3不能执行;当task1正常时候,task2不能执行,task3能执行。
task1报错时的运行结果:
task2正常时的运行结果:
第五节:Task构造函数之TaskCreationOptions枚举处理父子线程之间的关系。
一. 整体说明
揭秘:
通过F12查看Task类的源码(详见下面的截图),发现Task类的构造函数有有一个参数为:TaskCreationOptions类型,本章节可以算作是一个扩展章节,主要就来研究TaskCreationOptions类的作用。
该类主要用来处理父子线程之间的关系,重要的几个参数如下:
①.AttachedToParent:指定将任务附加到任务层次结构中的某个父级,父任务必须等待所有子任务执行完毕才能执行
(下面的例子task线程必须等task1和task2线程执行完毕才能执行)
②. DenyChildAttach: 不允许子任务附加到父任务上
(下面例子task不再等待task1和task2,和00的默认效果相同)
③. HideScheduler: 子任务不使用父类Task的Scheduler,而是使用默认的 (不进行测试)
④. LongRunning:当已知是长时间运行的任务,可以使用该选项 (不进行测试)
⑤. PreferFairness:类似于队列的感觉,尽可能公平的方式安排任务 (不进行测试)
源码如下:
1 // 摘要: 2 // 指定可控制任务的创建和执行的可选行为的标志。 3 [Serializable] 4 [Flags] 5 public enum TaskCreationOptions 6 { 7 // 摘要: 8 // 指定应使用默认行为。 9 None = 0, 10 // 11 // 摘要: 12 // 提示 System.Threading.Tasks.TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。 13 PreferFairness = 1, 14 // 15 // 摘要: 16 // 指定某个任务将是运行时间长、粗粒度的操作。 它会向 System.Threading.Tasks.TaskScheduler 提示,过度订阅可能是合理的。 17 LongRunning = 2, 18 // 19 // 摘要: 20 // 指定将任务附加到任务层次结构中的某个父级。 21 AttachedToParent = 4, 22 // 23 // 摘要: 24 // 如果尝试附有子任务到创建的任务,指定 System.InvalidOperationException 将被引发。 25 DenyChildAttach = 8, 26 // 27 // 摘要: 28 // 防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 System.Threading.Tasks.TaskScheduler.Default 29 // 当前计划程序。 30 HideScheduler = 16, 31 }
二. 实际测试
这里我们主要通过代码来比较默认情况下、AttachedToParent、DenyChildAttach之间的效果, task线程内部有task1和task2线程,并且在task内部开启。
1. 默认情况
1 { 2 Stopwatch watch = new Stopwatch(); 3 watch.Start(); 4 Console.WriteLine("----------------- Task多线程测试 --------------------------"); 5 Console.WriteLine("----------------- button1_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 6 7 #region 00-默认 8 { 9 Task task = new Task(() => 10 { 11 Task task1 = new Task(() => 12 { 13 Thread.Sleep(1000); 14 Console.WriteLine("我是task1线程"); 15 }); 16 Task task2 = new Task(() => 17 { 18 Thread.Sleep(1000); 19 Console.WriteLine("我是task2线程"); 20 }); 21 22 task1.Start(); 23 task2.Start(); 24 }); 25 26 task.Start(); 27 task.Wait(); //单个线程的等待 28 Console.WriteLine("------------------我是主线程--------------------"); 29 } 30 #endregion 31 32 watch.Stop(); 33 Console.WriteLine("----------------- button1_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 34 }
多次执行上述代码看效果:发现task线程执行完后,task1和task2才无序的执行。
2. AttachedToParent
作用:指定将任务附加到任务层次结构中的某个父级,父任务必须等待所有子任务执行完毕才能执行
1 { 2 Task task = new Task(() => 3 { 4 Task task1 = new Task(() => 5 { 6 Thread.Sleep(3000); 7 Console.WriteLine("我是task1线程"); 8 }, TaskCreationOptions.AttachedToParent); 9 Task task2 = new Task(() => 10 { 11 Thread.Sleep(3000); 12 Console.WriteLine("我是task2线程"); 13 }, TaskCreationOptions.AttachedToParent); 14 15 task1.Start(); 16 task2.Start(); 17 }); 18 19 task.Start(); 20 task.Wait(); //单个线程的等待 21 Console.WriteLine("------------------我是主线程--------------------"); 22 }
多次执行上述代码看效果:发现task线程必须等task1和task2执行完毕后才能执行(印证了AttachedToParent的作用),task1和task2无先后顺序。
3. DenyChildAttach
作用:不允许子任务附加到父任务上。
1 { 2 Task task = new Task(() => 3 { 4 Task task1 = new Task(() => 5 { 6 Thread.Sleep(3000); 7 Console.WriteLine("我是task1线程"); 8 }, TaskCreationOptions.AttachedToParent); 9 Task task2 = new Task(() => 10 { 11 Thread.Sleep(3000); 12 Console.WriteLine("我是task2线程"); 13 }, TaskCreationOptions.AttachedToParent); 14 15 task1.Start(); 16 task2.Start(); 17 }, TaskCreationOptions.DenyChildAttach); 18 19 task.Start(); 20 task.Wait(); //单个线程的等待 21 Console.WriteLine("------------------我是主线程--------------------"); 22 }
多次执行上述代码看效果:发现task线程执行完后,task1和task2才无序的执行。(和上述的默认情况是一致的)
第四节:Task的启动的四种方式以及Task、TaskFactory的线程等待和线程延续的解决方案
一. 背景
揭秘:
在前面的章节介绍过,Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面、资源占用方面、线程延续和阻塞方面、线程的取消方面等都显得比较笨拙,在面对复杂的业务场景下,显得有点捉襟见肘了。
正是在这种背景下,Task应运而生。
Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。
二. Task的4种启动方式
概要:
Task的启动有4种方式,其中3种异步启动开启一个新线程,1种同步启动的方式(有点和委托类似,BeginInvoke异步启动,Invoke同步启动),分别是:实例化的方式+Start方法启动、Task下Run方法启动、TaskFactory工厂的StartNew方法启动、Task下的同步方法RunSynchronously 启动。
1. 实例化的方式启动,调用Start方法
Task的构造函数中的参数是Action委托(注:不是Action<>多个重载),所以直接使用 ()=>{ }的方式传参,简洁明了,然后调用Start方式启动。
2. 调用Task类下的静态方法Run,进行启动
使用该方式启动,更加简洁,不需要实例化,也不需要调用Start方法,Run方法直接通过Action委托的方式进行传参即可(即: ()=>{} )。
3. TaskFactory工厂启动
使用TaskFactory工厂的StartNew方法启动,其中TaskFactory工厂可以直接实例化,或者 Task.Factory (推荐)。
4. 实例化方式RunSynchronously同步启动
Task实例化的方式,然后调用同步方法RunSynchronously ,进行线程启动。(PS: 类似委托开启线程,BeginInvoke是异步,而Invoke是同步)
三. Task的线程等待和延续
揭秘:
线程等待和延续通常情况放在一起来说,在同步方法中,即在单线程中,业务代码块按照从上往下的顺序执行,下面的代码块必须要等上面的代码块执行完毕后才能继续执行,这本身就是一种等待和延续,只不过是单线程内的等待和延续。
同理,来到多线程领域,这里的等待就不单单局限于代码块之间的等待和延续了,而是上升到某个线程 要等待 另外一个线程执行完毕后方能执行,这里特别说明一下,前面的章节提到线程等待基本上都是主线程在等子线程,当然,完全可能是子线程之间的相互等待和延续(实际上,这种情况更多)。
Task下的线程等待和延续主要以下几类:
①. Wait:针对单个Task的实例,可以task1.wait进行线程等待. <Task的实例方法>
②. WaitAny:执行的线程等待其中任何一个线程执行完毕即可执行(如果主线程执行,则卡主线程) <Task的静态方法>
③. WaitAll:执行的线程等待其中所有线程执行完毕方可执行(如果主线程执行,则卡主线程) <Task的静态方法>
④. WhenAny:与下面ContinueWith配合执行,当传入的线程中任何一个线程执行完毕,继续执行ContinueWith中的任务(属于开启新线程,不卡主线程) <Task的静态方法>
⑤. WhenAll:与下面ContinueWith配合执行,当传入的线程中所有线程执行完毕,继续执行ContinueWith中的任务(属于开启新线程,不卡主线程) <Task的静态方法>
⑥. ContinueWith:和上面WhenAny和WhenAll配合使用 <Task的实例方法>
1. WaitAny(执行的线程等待其中任何一个线程执行完毕即可执行)
这里给出线程等待加入集合中的代码,下面的线程等待通用这一部分代码,将不再列出。
2. WaitAll(执行的线程等待其中所有线程执行完毕方可执行)
3. WhenAny+ContinueWith
当其中一个线程执行完成后,新开启了一个线程执行,继续执行新业务,所以执行过程中,不卡主线程。
4. WhenAll+ContinueWith
当其中所有线程执行完成后,新开启了一个线程执行,继续执行新业务,所以执行过程中,不卡主线程。
四. TaskFactory的线程等待
说明: TaskFactory可以开启线程,当然也对应的线程的等待和延续。
①:ContinueWhenAny:等价于Task的WhenAny+ContinueWith
②:ContinueWhenAll:等价于Task的WhenAll+ContinueWith
1. ContinueWhenAny
2. ContinueWhenAll
第三节:ThreadPool的线程开启、线程等待、线程池的设置、定时功能
一. ThreadPool简介
ThreadPool简介:ThreadPool是一个线程池,当你需要开启n个线程时候,只需把这个指令抛给线程池,它将自动分配线程进行处理,它诞生于.Net 2.0时代。
ThreadPool与Thread的区别:
①:Thread每开启一个异步任务,就需要使用一个Thread,具有专一性,即使Thread已经死掉,仍然需要占用资源。
②:ThreadPool能实现n个线程处理n+m个异步任务,且没有死线程,默认都是初始化的。
二. 深究ThreadPool类
1:QueueUserWorkItem方法,将方法排入队列以便开启异步线程,它有两个重载。
a:QueueUserWorkItem(WaitCallback callBack),WaitCallback是一个有一个object类型参数且无返回值的委托
b:QueueUserWorkItem(WaitCallback callBack, object state),WaitCallback是一个有一个object类型参数且无返回值的委托,state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
使用方式:
a:严格的卡定义的写法,第一个重载和第二个重载均是处理有一个参数的函数,大于一个参数需要封装一个实体类,所需参数在实体类的构造函数中引入,然后调用第一个重载即可.
(不推荐这种写法)
通用写法: (可以处理任何参数个数的方法),这里不使用n即可。
ThreadPool.QueueUserWorkItem((n) =>
{
TestThread2(i.ToString(),i.ToString());
});
通用写法的简化版:
ThreadPool.QueueUserWorkItem(n => TestThread2(i.ToString(), i.ToString()));
代码如下:
2:线程等待(有局限性的)
利用ManualResetEvent类和WaitOne方法,多个线程的话需要写for循环。
运行结果:
3:常用方法
a:设置工作线程和IO线程的最大和最小值,SetMaxThreads和SetMinThreads。
b:获取工作线程和IO线程最大、最小、可用值,GetMaxThreads、GetMinThreads、GetAvailableThreads。
测试结果:
三. 扩展一个定时器功能
1:RegisterWaitForSingleObject类,但是不常用.(涉及到定时任务,建议使用Quartz.Net)
2:System.threading命名空间下的Thread类,通过查看源码,构造函数中有四个参数,第一个是object参数的委托,第二个是委托需要的值,
第三个是调用 callback 之前延迟的时间量(以毫秒为单位)
第四个是 调用 callback 的时间间隔(以毫秒为单位)
1 private void button12_Click(object sender, EventArgs e) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 Console.WriteLine("----------------- button_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 6 7 #region 01-RegisterWaitForSingleObject 8 //{ 9 // //每隔3s开启一个线程执行业务逻辑 10 // ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(true), new WaitOrTimerCallback((obj, b) => 11 // { 12 // //做逻辑判断,编写业务逻辑 13 // Console.WriteLine("obj={0},tid={1}, datetime={2}", obj, Thread.CurrentThread.ManagedThreadId, DateTime.Now); 14 15 // }), "hello world", 3000, false); 16 //} 17 #endregion 18 19 #region 02-Timer类 20 { 21 //2秒后开启该线程,然后每隔4s调用一次 22 System.Threading.Timer timer = new System.Threading.Timer((n) => 23 { 24 //书写业务逻辑 25 Console.WriteLine("我是子线程中的业务逻辑哦"); 26 }, "1", 2000, 4000); 27 } 28 #endregion 29 30 31 watch.Stop(); 32 Console.WriteLine("----------------- button_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 33 34 }
第二节:深入剖析Thread的五大方法、数据槽、内存栅栏。
一. Thread及其五大方法
Thread是.Net最早的多线程处理方式,它出现在.Net1.0时代,虽然现在已逐渐被微软所抛弃,微软强烈推荐使用Task(后面章节介绍),但从多线程完整性的角度上来说,我们有必要了解下N年前多线程的是怎么处理的,以便体会.Net体系中多线程处理方式的进化。
Thread中有五大方法,分别是:Start、Suspend、Resume、Intterupt、Abort
①.Start:开启线程
②.Suspend:暂停线程
③.Resume:恢复暂停的线程
④.Intterupt:中断线程(会抛异常,提示线程中断)
⑤.Abort:销毁线程
这五大方法使用起来,也比较简单,下面贴一段代码,体会一下如何使用即可。
在这里补充一下,在该系列中,很多测试代码中看到TestThread0、TestThread、TestThread2,分别对应无参、一个参数、两个参数的耗时方法,代码如下:
1 /// <summary> 2 /// 执行动作:耗时而已 3 /// </summary> 4 private void TestThread0() 5 { 6 Console.WriteLine("线程开始:当前线程的id为:{0},当前时间为:{1},", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")); 7 long sum = 0; 8 for (int i = 1; i < 999999999; i++) 9 { 10 sum += i; 11 } 12 Console.WriteLine("线程结束:当前线程的id为::{0},当前时间为:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")); 13 } 14 15 /// <summary> 16 /// 执行动作:耗时而已 17 /// </summary> 18 private void TestThread(string threadName) 19 { 20 Console.WriteLine("线程开始:线程名为:{2},当前线程的id为:{0},当前时间为:{1},", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName); 21 long sum = 0; 22 for (int i = 1; i < 999999999; i++) 23 { 24 sum += i; 25 } 26 Console.WriteLine("线程结束:线程名为:{2},当前线程的id为::{0},当前时间为:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName); 27 } 28 29 /// <summary> 30 /// 执行动作:耗时而已 31 /// </summary> 32 private void TestThread2(string threadName1, string threadName2) 33 { 34 Console.WriteLine("线程开始:线程名为:{2}和{3},当前线程的id为:{0},当前时间为:{1},", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName1, threadName2); 35 long sum = 0; 36 for (int i = 1; i < 999999999; i++) 37 { 38 sum += i; 39 } 40 Console.WriteLine("线程结束:线程名为:{2}和{3},当前线程的id为::{0},当前时间为:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName1, threadName2); 41 }
二. 从源码角度分析Thread类
(1) 分析Thread类的源码,发现其构造函数有两类,分别是ThreadStart和ParameterizedThreadStart类,
其中
①:ThreadStart类,是无参无返回值的委托。
②:ParameterizedThreadStart类,是有一个object类型参数但无返回值的委托.
使用方法:
①:针对ThreadStart类, ThreadStart myTs = () => TestThread(name); 然后再把myTs传入Thread的构造函数中
②:针对ParameterizedThreadStart类,ParameterizedThreadStart myTs = o => this.TestThread(o.ToString()); 然后再把myTs传入Thread的构造函数中
注:该方式存在拆箱和装箱的转换问题,不建议这么使用
通用写法:
Thread t = new Thread(() =>
{
Console.Write("333");
});
t.Start();
无须考虑Thread的构造函数,也不需要考虑Start的时候传参,直接使用()=>{}的形式,解决一切问题。
(二) 前台进程和后台进程(IsBackground属性)
①:前台进程,Thread默认为前台线程,程序关闭后,线程仍然继续,直到计算完为止
②:后台进程,将IsBackground属性设置为true,即为后台进程,主线程关闭,所有子线程无论运行完否,都马上关闭
(三) 线程等待(Join方法)
利用Join方法实现主线程等待子线程,当多个子线程进行等待的时候,只能通过for循环来实现
下面贴一下这三块设计到的代码:
1 private void button3_Click(object sender, EventArgs e) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 Console.WriteLine("-----------------Thread多线程 --------------------------"); 6 Console.WriteLine("----------------- button_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 7 List<Thread> threadList = new List<Thread>(); 8 for (int i = 0; i < 5; i++) 9 { 10 string name = string.Format("button1_Click{0}", i); 11 12 #region 方式一 13 { 14 ThreadStart myTs = () => TestThread(name); 15 Thread myThread = new Thread(myTs); 16 //Thread默认为前台线程,程序关闭后,线程仍然继续,直到计算完为止 17 myThread.IsBackground = true; //设置为后台线程,主程序关闭所有线程均关闭 18 myThread.Start(); 19 20 threadList.Add(myThread); 21 } 22 23 #endregion 24 25 #region 方式二 26 //{ 27 // ParameterizedThreadStart myTs = o => this.TestThread(o.ToString()); 28 // //ParameterizedThreadStart myTs = (o) => 29 // //{ 30 // // this.TestThread(o.ToString()); 31 // //}; 32 // Thread myThread = new Thread(myTs); 33 // myThread.Start(name); 34 35 // threadList.Add(myThread); 36 //} 37 38 #endregion 39 } 40 41 #region Thread线程等待 42 43 //利用join方法进行线程等待 44 foreach (Thread thread in threadList) 45 { 46 thread.Join(); 47 } 48 #endregion 49 50 watch.Stop(); 51 Console.WriteLine("----------------- button_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 52 53 }
(四). 扩展:Thread实现线程回调
三. 数据槽-线程可见性
背景:为了解决多线程竞用共享资源的问题,引入数据槽的概念,即将数据存放到线程的环境块中,使该数据只能单一线程访问.(属于线程空间上的开销)
下面的三种方式是解决多线程竞用共享资源的通用方式:
①:AllocateNamedDataSlot命名槽位和AllocateDataSlot未命名槽位 解决线程竞用资源共享问题。
(PS:在主线程上设置槽位,使该数据只能被主线程读取,其它线程无法访问)
private void button10_Click(object sender, EventArgs e) { #region 01-AllocateNamedDataSlot命名槽位 { var d = Thread.AllocateNamedDataSlot("userName"); //在主线程上设置槽位,使该数据只能被主线程读取,其它线程无法访问 Thread.SetData(d, "ypf"); //声明一个子线程 var t1 = new Thread(() => { Console.WriteLine("子线程中读取数据:{0}", Thread.GetData(d)); }); t1.Start(); //主线程中读取数据 Console.WriteLine("主线程中读取数据:{0}", Thread.GetData(d)); } #endregion #region 02-AllocateDataSlot未命名槽位 { var d = Thread.AllocateDataSlot(); //在主线程上设置槽位,使该数据只能被主线程读取,其它线程无法访问 Thread.SetData(d, "ypf"); //声明一个子线程 var t1 = new Thread(() => { Console.WriteLine("子线程中读取数据:{0}", Thread.GetData(d)); }); t1.Start(); //主线程中读取数据 Console.WriteLine("主线程中读取数据:{0}", Thread.GetData(d)); } #endregion }
②:利用特性[ThreadStatic] 解决线程竞用资源共享问题
(PS:在主线程中给ThreadStatic特性标注的变量赋值,则只有主线程能访问该变量)
③:利用ThreadLocal线程的本地存储, 解决线程竞用资源共享问题(线程可见性)
(PS: 在主线程中声明ThreadLocal变量,并对其赋值,则只有主线程能访问该变量)
四. 内存栅栏-线程共享资源
背景:当多个线程共享一个变量的时候,在Release模式的优化下,子线程会将共享变量加载的cup Cache中,导致主线程不能使用该变量而无法运行。
解决方案:
①:不要让多线程去操作同一个共享变量,从根本上解决这个问题。
②:利用MemoryBarrier方法进行处理,在此方法之前的内存写入都要及时从cpu cache中更新到 memory;在此方法之后的内存读取都要从memory中读取,而不是cpu cache。
③:利用VolatileRead/Write方法进行处理。
1 private void button11_Click(object sender, EventArgs e) 2 { 3 #region 01-默认情况(Release模式主线程不能正常运行) 4 //{ 5 // var isStop = false; 6 // var t = new Thread(() => 7 // { 8 // var isSuccess = false; 9 // while (!isStop) 10 // { 11 // isSuccess = !isSuccess; 12 // } 13 // Console.WriteLine("子线程执行成功"); 14 // }); 15 // t.Start(); 16 17 // Thread.Sleep(1000); 18 // isStop = true; 19 20 // t.Join(); 21 // Console.WriteLine("主线程执行结束"); 22 //} 23 #endregion 24 25 #region 02-MemoryBarrier解决共享变量(Release模式下可以正常运行) 26 //{ 27 // var isStop = false; 28 // var t = new Thread(() => 29 // { 30 // var isSuccess = false; 31 // while (!isStop) 32 // { 33 // Thread.MemoryBarrier(); 34 35 // isSuccess = !isSuccess; 36 // } 37 // Console.WriteLine("子线程执行成功"); 38 // }); 39 // t.Start(); 40 41 // Thread.Sleep(1000); 42 // isStop = true; 43 44 // t.Join(); 45 // Console.WriteLine("主线程执行结束"); 46 //} 47 #endregion 48 49 #region 03-VolatileRead解决共享变量(Release模式下可以正常运行) 50 { 51 var isStop = 0; 52 var t = new Thread(() => 53 { 54 var isSuccess = false; 55 while (isStop == 0) 56 { 57 Thread.VolatileRead(ref isStop); 58 59 isSuccess = !isSuccess; 60 } 61 Console.WriteLine("子线程执行成功"); 62 }); 63 t.Start(); 64 65 Thread.Sleep(1000); 66 isStop = 1; 67 68 t.Join(); 69 Console.WriteLine("主线程执行结束"); 70 } 71 #endregion 72 73 74 }
第一节:复习委托,并且通过委托的异步调用开启一个新线程和异步回调、异步等待。
一. 再谈委托
1. 委托是一个关键字为delegate的自定义类型,通过委托可以把方法以参数的形式传递给另外一个方法,实现插件式的开发模式;
同时调用委托的时候,委托所包含的所有方法都会被实现。
2. 委托的发展历史:new实例化传递方法→直接等于方法名→delegate匿名方法→省略delegate→省略括号中的参数→当只有一个参数省略小括号
→当方法体只有一行,省略大括号
(详见:http://www.cnblogs.com/yaopengfei/p/6959141.html)
3:常用的Action委托和Func委托
A. Action<>委托,无返回值,至少有一个参数的委托
B. Func<>委托,有返回值,可以无参数的委托(当然也可以有参数)
C. Action委托,无参数无返回值的委托
二. 委托的调用
委托的调用分为两种:
A. 同步调用:Invoke方法,方法参数为函数的参数。
B. 异步调用:BeginInvoke方法。
其中无论是哪类调用,都有两类写法:
①:利用Action<>(或Func<>)内置委托,调用的时候赋值。
②:利用Action委托,直接赋值,然后调用。
1 /// <summary> 2 /// 执行动作:耗时而已 3 /// </summary> 4 private void TestThread2(string threadName1, string threadName2) 5 { 6 Console.WriteLine("线程开始:线程名为:{2}和{3},当前线程的id为:{0},当前时间为:{1},", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName1, threadName2); 7 long sum = 0; 8 for (int i = 1; i < 999999999; i++) 9 { 10 sum += i; 11 } 12 Console.WriteLine("线程结束:线程名为:{2}和{3},当前线程的id为::{0},当前时间为:{1}", System.Threading.Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff"), threadName1, threadName2); 13 }
三. 深入剖析BeginInvoke方法
首先需要明确,该方法参数个数不定, 最后两个参数含义固定,如果不使用的话,需要赋值null;该方法最少两个参数,即方法无参数,这种情况下BeginInvoke中只有两个参数。此外,赋值的方法有几个参数,BeginInvoke中从左开始,新增几个参数。
①. 倒数第二个参数:是有一个参数值无返回值的委托,它代表的含义为,该线程执行完毕后的回调。
②. 倒数第一个参数:向倒数第二个参数(即回调)中传值,需要用AsyncState来接受。
③. 其它参数:即为赋值方法的参数。
注:BeginInvoke的返回值等价于异步回调中的t。
1 private void button13_Click(object sender, EventArgs e) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 Console.WriteLine("----------------- button1_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 6 7 Action<string> myFunc = this.TestThread; 8 IAsyncResult asyncResult = null; 9 //参数说明:前面几个参数都是方法的参数值,倒数第二个为异步调用的回调函数,倒数第一个为传给回调函数的参数 10 for (int i = 0; i < 1; i++) 11 { 12 string name = string.Format("button1_Click{0}", i); 13 asyncResult = myFunc.BeginInvoke(name, t => 14 { 15 Console.WriteLine("我是线程{0}的回调", Thread.CurrentThread.ManagedThreadId); 16 //用 t.AsyncState 来获取回调传进来的参数 17 Console.WriteLine("传进来的参数为:{0}", t.AsyncState); 18 19 //测试一下异步返回值的结果 20 Console.WriteLine("异步返回值的结果:{0}", t.Equals(asyncResult)); 21 }, "maru"); 22 } 23 24 watch.Stop(); 25 Console.WriteLine("----------------- button1_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 26 27 }
结果:
四. 线程等待的三种方式
1. asyncResult.IsCompleted属性,该方式会存在时间上的误差。
2. WaitOne方法,可以控制一直等待or超时不再等待。
3. EndInvoke方法,官方推荐的线程等待的方式。
以上三种方式的局限性:批量线程等待的时候,不灵活,需要for循环了。
1 private void button14_Click(object sender, EventArgs e) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 Console.WriteLine("----------------- button1_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 6 7 IAsyncResult asyncResult = null; 8 Action<string> myFunc = this.TestThread; 9 string name = string.Format("button1_Click{0}", 111); 10 asyncResult = myFunc.BeginInvoke(name, t => 11 { 12 Console.WriteLine("我是线程{0}的回调", Thread.CurrentThread.ManagedThreadId); 13 //用 t.AsyncState 来获取回调传进来的参数 14 Console.WriteLine("传进来的参数为:{0}", t.AsyncState); 15 }, "maru"); 16 17 //等待的方式1:会有时间上的误差 18 //while (!asyncResult.IsCompleted) 19 //{ 20 // Console.WriteLine("正在等待中"); 21 //} 22 23 // 等待的方式二: 24 //asyncResult.AsyncWaitHandle.WaitOne();//一直等待 25 //asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待 26 //asyncResult.AsyncWaitHandle.WaitOne(1000);//等待1000毫秒,超时就不等待了 27 28 //等待的方式三: 29 myFunc.EndInvoke(asyncResult); 30 31 watch.Stop(); 32 Console.WriteLine("----------------- button1_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 33 34 }
下面是多个线程等待的情况:
1 private void button15_Click(object sender, EventArgs e) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 Console.WriteLine("----------------- button1_Click 开始 主线程id为:{0} --------------------------", Thread.CurrentThread.ManagedThreadId); 6 7 List<IAsyncResult> list = new List<IAsyncResult>(); 8 9 for (int i = 0; i < 5; i++) 10 { 11 string name = string.Format("button1_Click{0}", i); 12 Action myFunc = () => 13 { 14 TestThread2(name, name); 15 }; 16 var asyncResult = myFunc.BeginInvoke(null, null); 17 list.Add(asyncResult); 18 } 19 20 //下面是线程等待 21 foreach (var item in list) 22 { 23 item.AsyncWaitHandle.WaitOne(-1); 24 } 25 26 watch.Stop(); 27 Console.WriteLine("----------------- button1_Click 结束 主线程id为:{0} 总耗时:{1}--------------------------", Thread.CurrentThread.ManagedThreadId, watch.ElapsedMilliseconds); 28 }
结果: