线程基础
1. Windows为什么要支持线程
线程(thread)职责是对CPU进行虚拟化.
Windows为每个进程提供了该进程专用的线程(功能相当于一个CPU, 可将线程理解为一个逻辑CPU).
如果应用程序进入无限循环,与代码相关的进程会被“冻结”,但其他进程可以继续执行.
2.专用线程执行异步计算限制操作
创建一个线程, 并让其执行一次异步计算限制(asunchronous compute-bound)操作
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace thread1 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 Console.WriteLine("MainThread: start a dedicated thread " + "to do an asynchronous operation"); 14 Thread dedicatedThread = new Thread(ComputerBoundOp); //生成专用线程 15 dedicatedThread.Start(555); //Thread的Start方法开始执行回调函数, 异步调用一个方法 16 17 Console.WriteLine("MainThread: Doing Other Work here..."); 18 Thread.Sleep(10000); 19 dedicatedThread.Join(); //代表dedicatedThread线程结束后才能继续执行下面代码 20 Console.WriteLine("The End"); 21 } 22 23 private static void ComputerBoundOp(Object state){ 24 Console.WriteLine("ComputerBoundOp: state={0}",state); 25 Thread.Sleep(1000); //这个方法返回后,专用线程将结束 26 } 27 } 28 }
有可能后两句话的打印顺序相反, 因为无法控制Windows对两个线程进行调度的方式.
但是试了几次都是这样的结果( 妈蛋!)
dedicatedThread.join(); 直到dedicatedThread线程销毁或终止, 才会解除阻塞.
3. 前台线程和后台线程
每个线程要么是前台线程, 要么是后台线程.
一个进程中所有前台线程结束运行时, 任何后台线程将强制终止, 不会抛出异常
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace thread1 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 Thread t = new Thread(Worker);//默认为前台线程 14 t.IsBackground = true; //使线程成为后台线程 15 t.Start(); 16 //如果t是一个前台线程,则应用程序大约10秒后才终止
//如果t是一个后台线程,则应用程序立即终止 17 Console.WriteLine("Returning from Main"); 18 } 19 20 private static void Worker() 21 { 22 Thread.Sleep(10000); //模拟做10秒钟工作 23 Console.WriteLine("Returning from Worker"); 24 } 25 } 26 }
结果就是程序立即终止了
4. 线程池
创建和销毁线程都是一个昂贵的操作, 需要耗费大量时间, 太多的线程会浪费内存资源.
利用线程池可以对线程进行有效的控制,使得线程能够更好的协作。
线程池将自己的线程划分为 Worker线程 和 I/O线程
4.1 线程池简单的计算限制操作
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace thread1 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 Console.WriteLine("MainThread: queuing an asynchronous operation"); 14 ThreadPool.QueueUserWorkItem(ComputerBoundOp,555); 15 Console.WriteLine("MainThread: Doing Other Work here..."); 16 Thread.Sleep(10000); 17 Console.WriteLine("The End"); 18 } 19 20 private static void ComputerBoundOp(Object state){ 21 Console.WriteLine("ComputerBoundOp: state={0}",state); 22 Thread.Sleep(1000); 23 } 24 } 25 }
ThreadPool类可将异步的, 计算限制的操作放到线程池队列中
1 public static bool QueueUserWorkItem(WaitCallback callBack); 2 public static bool QueueUserWorkItem(WaitCallback callBack, object state);
那么, 线程池是如何有效的控制系统资源的消耗的呢?
它的原理非常简单:
- 线程池中维护一个请求队列,当应用程序有异步的请求时,将此请求(比如请求A)发送到线程池。
- 线程池将请求A放入请求队列中,然后新建一个线程(比如线程A)来处理请求A。
- 请求A处理完成后,线程池不会销毁线程A,而是使用线程A来处理请求队列中的下一个请求(比如请求B) (销毁线程会耗费资源)。
- 当请求过多时,线程池才会再新建一些线程来加快处理请求队列中的请求。(注1)
- 当请求队列为空时,线程池会销毁一些空闲时间比较长的线程。(注2)
注1:保证所有的请求由少量线程处理,减少系统资源的消耗,同时减少了线程新建,销毁的次数。
注2:空闲时间多长才销毁线程是由CLR决定的,不同版本的CLR这个时间可能不同。
5. Task
参考博客 http://www.cnblogs.com/wang_yb/archive/2011/11/10/2244745.html
利用ThreadPool的QueueUserWorkItem方法建立的异步操作存在一些限制:
1. 异步操作没有返回值
2. 没有内建的机制通知异步操作什么时候完成
通过任务的状态(TaskStatus),可以了解任务(Task)的生命周期。
1 public enum TaskStatus 2 { 3 // 运行前状态 4 Created = 0, // 任务被显式创建,通过Start()开始这个任务 5 WaitingForActivation = 1, // 任务被隐式创建,会自动开始 6 WaitingToRun = 2, // 任务已经被调度,但是还没有运行 7 8 // 运行中状态 9 Running = 3, // 任务正在运行 10 WaitingForChildrenToComplete = 4, // 等待子任务完成 11 12 // 运行完成后状态 13 RanToCompletion = 5, // 任务正常完成 14 Canceled = 6, // 任务被取消 15 Faulted = 7, // 任务出错 16 }
构造一个Task后,它的状态为Create。
启动后,状态变为WaitingToRun。
实际在一个线程上运行时,状态变为Running。
运行完成后,根据实际情况,状态变为RanToCompletiion,Canceled,Faulted三种中的一种。
如果Task不是通过new来创建的,而是通过以下某个函数创建的,那么它的状态就是WaitingForActivation:
ContinueWith,ContinueWhenAll,ContinueWhenAny,FromAsync。
如果Task是通过构造一个TaskCompletionSource<TResult>对象来创建的,该Task在创建时也是处于WaitingForActivation状态。
5.1 创建并启动一个Task
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("MainThread Start"); 15 16 Task t = new Task(() => 17 { 18 Console.WriteLine("Task Start"); 19 Thread.Sleep(1000); 20 Console.WriteLine("Task End"); 21 }); 22 23 t.Start(); 24 25 //主线程没有等待Task, 在Task完成之前就已经完成了 26 Console.WriteLine("Main Thread end!"); 27 Console.ReadKey(true); 28 } 29 } 30 }
5.2 主线程等待子线程完成
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("MainThread Start"); 15 16 Task t1 = new Task(() => 17 { 18 Console.WriteLine("Task1 Start"); 19 Thread.Sleep(1000); 20 Console.WriteLine("Task1 End"); 21 }); 22 Task t2 = new Task(() => 23 { 24 Console.WriteLine("Task2 Start"); 25 Thread.Sleep(2000); 26 Console.WriteLine("Task2 End"); 27 }); 28 t1.Start(); 29 t2.Start(); 30 31 // 当t1和t2中任何一个完成后,主线程继续后面的操作 32 // Task.WaitAny(new Task[] { t1, t2 }); 33 34 //当t1和t2中全部完成后,主线程继续后面的操作 35 Task.WaitAll(new Task[] { t1, t2 }); 36 37 //主线程没有等待Task, 在Task完成之前就已经完成了 38 Console.WriteLine("Main Thread end!"); 39 Console.ReadKey(true); 40 } 41 } 42 }
5.3取消Task
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("Main Thread start!"); 15 CancellationTokenSource cts = new CancellationTokenSource(); 16 17 // 创建2个Task 18 Task t1 = new Task(() => 19 { 20 Console.WriteLine("Task1 start"); 21 for (int i = 0; i < 100; i++) 22 { 23 if (!cts.Token.IsCancellationRequested) 24 { 25 Console.WriteLine("Count : " + i.ToString()); 26 Thread.Sleep(1000); 27 } 28 else 29 { 30 Console.WriteLine("Task1 is Cancelled!"); 31 break; 32 } 33 } 34 Console.WriteLine("Task1 end"); 35 }, cts.Token); 36 37 // 启动Task 38 t1.Start(); 39 Thread.Sleep(3000); 40 // 运行3秒后取消Task 41 cts.Cancel(); 42 43 // 为了测试取消操作,主线程等待Task完成 44 Task.WaitAny(new Task[] { t1 }); 45 Console.WriteLine("Main Thread end!"); 46 Console.ReadKey(true); //等待按键结束 47 } 48 } 49 }
6. 子任务和任务工厂
6.1延续任务
为了保证程序的伸缩性, 应该尽量避免线程阻塞, 这就意味着我们在等待一个任务完成时, 最好不要用Wait, 而是让一个任务结束后自动启动它的下一个任务.
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("Main Thread start!"); 15 16 // 第一个Task 17 Task<int> t1 = new Task<int>(() => 18 { 19 Console.WriteLine("Task 1 start!"); 20 Thread.Sleep(2000); 21 Console.WriteLine("Task 1 end!"); 22 return 1; 23 }); 24 25 // 启动第一个Task 26 t1.Start(); 27 28 // 因为TaskContinuationOptions.OnlyOnRanToCompletion, 29 // 所以第一个Task正常结束时,启动第二个Task。 30 // TaskContinuationOptions.OnlyOnFaulted,则第一个Task出现异常时,启动第二个Task 31 // 其他可详细参考TaskContinuationOptions定义的各个标志 32 33 t1.ContinueWith(AnotherTask, TaskContinuationOptions.OnlyOnRanToCompletion);//OnlyOnRanToComplete可以更改为其他参数,如OnlyOnCanceled 34 35 Console.WriteLine("Main Thread end!"); 36 Console.ReadKey(true); 37 } 38 39 // 第二个Task的处理都在AnotherTask函数中, 40 // 第二个Task的引用其实就是上面ContinueWith函数的返回值。 41 // 这里没有保存第二个Task的引用 42 private static void AnotherTask(Task<int> task) 43 { 44 Console.WriteLine("Task 2 start!"); 45 Thread.Sleep(1000); 46 Console.WriteLine("Task 1's return Value is : " + task.Result); //Task参数传递 47 Console.WriteLine("Task 2 end!"); 48 } 49 } 50 }
6.2 子任务
定义子任务时,注意一定要加上TaskCreationOptions.AttachedToParent,这样父任务会等待子任务执行完后才结束
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("Main Thread start!"); 15 16 Task<int[]> parentTask = new Task<int[]>(() => 17 { 18 var result = new int[3]; 19 20 // 子任务1 21 new Task(() => 22 { 23 Console.WriteLine("sub task 1 start!"); 24 Thread.Sleep(1000); 25 Console.WriteLine("sub task 1 end!"); 26 result[0] = 1; 27 }, TaskCreationOptions.AttachedToParent).Start(); 28 29 // 子任务2 30 new Task(() => 31 { 32 Console.WriteLine("sub task 2 start!"); 33 Thread.Sleep(1000); 34 Console.WriteLine("sub task 2 end!"); 35 result[1] = 2; 36 }, TaskCreationOptions.AttachedToParent).Start(); 37 38 // 子任务3 39 new Task(() => 40 { 41 Console.WriteLine("sub task 3 start!"); 42 Thread.Sleep(1000); 43 Console.WriteLine("sub task 3 end!"); 44 result[2] = 3; 45 }, TaskCreationOptions.AttachedToParent).Start(); 46 47 return result; 48 }); 49 50 parentTask.Start(); 51 52 Console.WriteLine("Parent Task's Result is :"); 53 foreach (int result in parentTask.Result) 54 Console.Write("{0}\t", result); 55 56 Console.WriteLine(); 57 Console.WriteLine("Main Thread end!"); 58 Console.ReadKey(true); 59 } 60 } 61 }
<1>3个子任务的执行顺序也和定义的顺序无关,比如任务3可能最先执行(与CPU的调度有关)。
<2>Result打印出来是1 2 3 ,把 ,TaskCreationOptions.AttachedToParent 删掉试试,打印出来的Result应该是3个0,而不是1 2 3。
6.3 任务工厂
可以使用任务工厂来批量创建任务
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace thread1 9 { 10 class Program 11 { 12 static void Main(string[] args) 13 { 14 Console.WriteLine("Main Thread start!"); 15 16 Task<int[]> parentTask = new Task<int[]>(() => 17 { 18 var result = new int[3]; 19 TaskFactory tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.None); 20 21 // 子任务1 22 tf.StartNew(() => 23 { 24 Console.WriteLine("sub task 1 start!"); 25 Thread.Sleep(1000); 26 Console.WriteLine("sub task 1 end!"); 27 result[0] = 1; 28 }); 29 30 // 子任务2 31 tf.StartNew(() => 32 { 33 Console.WriteLine("sub task 2 start!"); 34 Thread.Sleep(1000); 35 Console.WriteLine("sub task 2 end!"); 36 result[1] = 2; 37 }); 38 39 // 子任务3 40 tf.StartNew(() => 41 { 42 Console.WriteLine("sub task 3 start!"); 43 Thread.Sleep(1000); 44 Console.WriteLine("sub task 3 end!"); 45 result[2] = 3; 46 }); 47 48 return result; 49 }); 50 51 parentTask.Start(); 52 53 Console.WriteLine("Parent Task's Result is :"); 54 foreach (int result in parentTask.Result) 55 Console.Write("{0}\t", result); 56 57 Console.WriteLine(); 58 Console.WriteLine("Main Thread end!"); 59 Console.ReadKey(true); 60 } 61 } 62 }
使用任务工厂与上面3.2中直接定义子任务相比,优势主要在于可以共享子任务的设置,比如在TaskFactory中设置TaskCreationOptions.AttachedToParent, 那么它启动的子任务都具有这个属性了。
当然,任务工厂(TaskFactory)还提供了很多控制子任务的函数,用的时候可以看看它的类定义。
7. Parallel并行执行任务
1 Console.WriteLine("Main Thread start!"); 2 int max = 10; 3 4 // 普通循环 5 long start = Stopwatch.GetTimestamp(); 6 for (int i = 0; i < max; i++) 7 { 8 Thread.Sleep(1000); 9 } 10 Console.WriteLine("{0:N0}", Stopwatch.GetTimestamp() - start); 11 12 // 并行的循环 13 start = Stopwatch.GetTimestamp(); 14 Parallel.For(0, max, i => { Thread.Sleep(1000); }); 15 Console.WriteLine("{0:N0}", Stopwatch.GetTimestamp() - start); 16 17 Console.WriteLine("Main Thread end!"); 18 Console.ReadKey(true);
Parallel是为了简化任务编程而新增的静态类,利用Parallel可以将平时的循环操作都并行起来
在上面的例子中,采用并行循环消耗的时间不到原先的一半。
但是,采用并行循环需要满足一个条件,就是for循环中的内容能够并行才行。
比如for循环中是个对 循环变量i 进行的累加操作(例如sum += i;),那就不能使用并行循环。
还有一点需要注意,Parallel的方法本身有开销。
所以如果for循环内的处理比较简单的话,那么直接用for循环可能更快一些。
比如将上例中的Thread.Sleep(1000);删掉,再运行程序发现,直接for循环要快很多。