C# 线程与进程
进程与线程
进程
当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源,如Window句柄,文件系统句柄或其他内核对象。每个进程都分配的虚拟内存。
而一个进程又是由多个线程所组成的。
可以打开计算机设备管理查看自己电脑CPU数目,Ctrl+Alt+.调出任务管理器也可以查看,任务管理器还有详细的目前进程数目和线程数目。
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
线程
线程是程序中独立的指令流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),一个进程的内存空间是共享的,每个线程都可以使用这些共享空间。即不同的线程可以执行同样的函数。
任何程序在执行时,至少有一个主线程。
多线程
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
一个进程的多个线程可以同时运行在不同的CPU上或多核CPU的不同内核上。
多线程的好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
多线程的不利方面:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
- 多线程需要协调和管理,所以需要CPU时间跟踪线程
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
- 线程太多会导致控制太复杂,最终可能造成很多Bug
操作系统的设计
归结为三点:
- 以多进程形式,允许多个任务同时运行
- 以多线程形式,允许单个任务分成不同的部分运行
- 提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
利用异步委托去创建线程
创建线程的一种简单方式是定义一个委托,并异步调用它。
委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。接下来定义一个方法,使用委托异步调用(开启一个线程去执行这个方法)。
当我们通过BeginInvoke开启一个异步委托的时候,返回的结果是IAsyncResult,我们可以通过它的AsyncWaitHandle属性访问等待句柄。
1 static string TakesAWhile(int times) 2 { 3 Console.WriteLine("异步函数开始!"); 4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒 5 return "异步结束!"; 6 } 7 public delegate string TakesAWhileDelegate(int ms);// 声明委托 8 static void Main(string[] args) 9 { 10 TakesAWhileDelegate _delegateTasks = TakesAWhile; 11 IAsyncResult ar = _delegateTasks.BeginInvoke(3000, null, null); 12 while (ar.IsCompleted == false) 13 { 14 Console.Write("-"); 15 Thread.Sleep(10);//每隔10mswhile循环一次,Thread类控制线程 16 } 17 Console.WriteLine(_delegateTasks.EndInvoke(ar)); 18 }
这个属性返回一个WaitHandler类型的对象,它中的WaitOne()方法可以等待委托线程完成其任务,WaitOne方法可以设置一个超时时间作为参数(要等待的最长时间),如果发生超时就返回false。
1 static string TakesAWhile(int times) 2 { 3 Console.WriteLine("异步函数开始!"); 4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒 5 return "异步结束!"; 6 } 7 public delegate string TakesAWhileDelegate(int ms);// 声明委托 8 static void Main(string[] args) 9 { 10 11 TakesAWhileDelegate _delegateTasks = TakesAWhile; 12 IAsyncResult ar = _delegateTasks.BeginInvoke(3000, null, null); 13 while (true) 14 { 15 Console.Write("-"); 16 if (ar.AsyncWaitHandle.WaitOne(10, false))// 等待每隔10ms,如果当前异步操作执行完毕时就会返回一个true 17 { 18 Console.WriteLine("异步结束!"); 19 break; 20 } 21 } 22 Console.WriteLine(_delegateTasks.EndInvoke(ar)); 23 }
等待委托的结果的第3种方式是使用异步回调。在BeginInvoke的第三个参数中,可以传递一个满足AsyncCallback委托的方法,AsyncCallback委托定义了一个IAsyncResult类型的参数其返回类型是void。
对于最后一个参数,可以传递任意对象,以便从回调方法中访问它。(我们可以设置为委托实例,这样就可以在回调方法中获取委托方法的结果)
格式如下:
委托对象.BeginInvoke(委托的参数列表, 回调函数,对象);
例子如下:
1 static string TakesAWhile(int times) 2 { 3 Console.WriteLine("异步函数开始!"); 4 Thread.Sleep(times);//程序运行到这里的时候会暂停ms毫秒 5 return "异步结束!"; 6 } 7 public delegate string TakesAWhileDelegate(int ms);// 声明委托 8 static void Main(string[] args) 9 { 10 11 TakesAWhileDelegate _delegateTasks = TakesAWhile; 12 13 _delegateTasks.BeginInvoke(3000, TakesAWhileCompleted,_delegateTasks); 14 while (true) 15 { 16 Console.Write("-"); 17 Thread.Sleep(10); 18 } 19 20 } 21 static void TakesAWhileCompleted(IAsyncResult ar) 22 {//回调方法是从委托线程中调用的,并不是从主线程调用的,可以认为是委托线程最后要执行的程序 23 if (ar == null) throw new ArgumentNullException("ar"); 24 TakesAWhileDelegate _temp = ar.AsyncState as TakesAWhileDelegate; 25 Console.Write(_temp.EndInvoke(ar)); 26 }
可以使用在BeginInvoke中使用Lambda表达式,更方便:
1 _delegateTasks.BeginInvoke(3000, ar => Console.WriteLine(_delegateTasks.EndInvoke(ar)), null); 2 while (true) 3 { 4 Console.Write("-"); 5 Thread.Sleep(10); 6 }
开启BeginInvoke后,判断线程是否结束的方法,总结如下:
- 利用IAsyncResult中的IsCompleted属性判断是否完成
- 获取IAsyncResult中的AsyncWaitHandle.WaitOne()线程超时是否,超时的返回参数true
- 利用BeginInvoke的第三个参数AsyncCallback委托的方法
利用Thread类去创建和控制线程
MSDN查阅地址:
https://msdn.microsoft.com/zh-cn/library/system.threading.thread(v=VS.95).aspx
使用Thread类可以创建和控制线程,并获取其状态。Thread构造函数的参数是一个的委托类型。
例如:
1 static void Main(string[] args) 2 { 3 Thread _nextThread = new Thread(ThreadNext); 4 _nextThread.Start();//线程开启 5 Console.WriteLine("主线程运行!"); 6 Thread.Sleep(50); 7 _nextThread.Abort();//终止线程 8 Console.WriteLine("线程终止!"); 9 } 10 static void ThreadNext() { 11 while (true) 12 { 13 Console.WriteLine("线程开启—"); 14 } 15 }
若要向Thread类传值,有两种方法,第一种:使用带ParameterizedThreadStart委托参数的Thread构造函数
官方描述的Thread类两个构造函数
需注意:线程不会在创建时开始执行。 若要为执行而调度线程,请调用 Start 方法。 若要将数据对象传递给线程,请使用 Start(Object) 方法重载。
特别注意:传递的值为Object对象!
使用例子:
1 static void Main(string[] args) 2 { 3 string str ="线程进行--"; 4 Thread _nextThread = new Thread(ThreadNext); 5 _nextThread.Start(str);//传入一个对象 6 Console.WriteLine("主线程运行!"); 7 Thread.Sleep(20); 8 _nextThread.Abort();//终止线程 9 Console.WriteLine("线程终止!"); 10 } 11 static void ThreadNext(object str) {//这里参数类型必须为object对象! 12 while (true) 13 { 14 Console.WriteLine(str); 15 } 16 }
还有一种方法:初始化一个对象,对象内部的方法去调用对象里面的成员,线程方法(实例方法)就可以调用内部成员达到传值的目的。
使用例子如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 UserThread myThread = new UserThread("线程开启"); 6 Thread _nextThread = new Thread(myThread.WriteMessage); 7 _nextThread.Start();//传入一个对象 8 Console.WriteLine("主线程运行!"); 9 Thread.Sleep(20); 10 _nextThread.Abort();//终止线程 11 Console.WriteLine("线程终止!"); 12 } 13 } 14 class UserThread 15 { 16 private string message; 17 public UserThread(string message) 18 { 19 this.message = message; 20 } 21 public void WriteMessage() 22 { 23 while (true) 24 { 25 Console.WriteLine(message); 26 } 27 28 } 29 }
线程的控制
在默认情况下,用Thread类创建的线程是前台线程。线程池中的线程总是后台线程。
常用的属性和方法:
- 线程前后台的控制。在用Thread类创建线程的时候,可以设置IsBackground属性,表示它是一个前台线程还是一个后台线程。
- 线程的优先级。Thead类中设置Priority属性,以影响线程的基本优先级 ,Priority属性是一个ThreadPriority枚举定义的一个值。定义的级别有Highest ,AboveNormal,BelowNormal 和 Lowest
- 线程插入。可以调用Thread对象的Join方法,表示把Thread加入进来,停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止
- 终止线程。使用Thread对象的Abort()方法可以停止线程。
- 开始线程的。将当前实例的状态更改为 ThreadState.Running。
- 睡眠当前线程。Thread.Sleep()方法可以让当前线程休眠进入WaitSleepJoin状态
线程争用问题:
当程序较大时,运行程序的时候,在计算机有限的资源下,无法避免会产生多个线程争用的问题,对数据进行多次或没有修改。
解决方案为使用Lock关键字,锁住引用对象。Lock只能锁引用对象!
操作如下:
当数据非应用类型,我们可以通过在对象初始化时,同时初始化一个object类型的变量sync,每次修改数据对象时都先锁定sync对象。
使用例子:
static void RaceCondition(object o ){ StateObject state = o as StateObject; int i = 0; while(true){ lock(state){ state.ChangeState(i++); } } }
或者
private object sync = new object(); public void ChangeState(int loop){ lock(sync){ if(state==5){ state++; Console.WriteLine("State==5:"+state==5+" Loop:"+loop); } state = 5; } }
线程死锁问题:
同时出现两个锁,两个线程都在等另一个线程解锁。
1 public class SampleThread{ 2 private StateObject s1; 3 private StateObject s2; 4 public SampleThread(StateObject s1,StateObject s2){ 5 this.s1= s1; 6 this.s2 = s2; 7 } 8 public void Deadlock1(){ 9 int i =0; 10 while(true){ 11 lock(s1){ 12 lock(s2){ 13 s1.ChangeState(i); 14 s2.ChangeState(i); 15 i++; 16 Console.WriteLine("Running i : "+i); 17 } 18 } 19 } 20 } 21 public void Deadlock2(){ 22 int i =0; 23 while(true){ 24 lock(s2){ 25 lock(s1){ 26 s1.ChangeState(i); 27 s2.ChangeState(i); 28 i++; Console.WriteLine("Running i : "+i); 29 } 30 } 31 } 32 } 33 } 34 var state1 = new StateObject(); 35 var state2 = new StateObject(); 36 new Task(new SampleTask(s1,s2).DeadLock1).Start(); 37 new Task(new SampleTask(s1,s2).DeadLock2).Start();
解决方法就是一开始就设定好锁定的先后顺序。
线程池
线程池不需要自己创建,ThreadPool类是系统提供管理线程的线程池类。这个类会在需要时增减池中线程的线程数,直到达到最大的线程数。 池中的最大线程数是可配置的。
创建线程需要时间。 如果有不同的小任务要完成,就可以事先创建许多线程 , 在应完成这些任务时发出请求。 这个线程数最好在需要更多的线程时增加,在需要释放资源时减少。。
在双核 CPU中 ,默认设置为1023个工作线程和 1000个 I/o线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。 如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
任务
在.NET4 新的命名空间System.Threading.Tasks包含了类抽象出了线程功能,在后台使用的ThreadPool进行管理的。
任务表示应完成某个单元的工作。这个工作可以在单独的线程中运行,也可以以同步方式启动一个任务。
启动任务三种方法:
///TaskMethod表示一个委托方法 /// <summary> /// 启动任务t1 /// </summary> TaskFactory _Taskfactory = new TaskFactory(); Task t1 = _Taskfactory .StartNew(TaskMethod); /// <summary> /// 启动任务t2 /// </summary> Task t2 = TaskFactory.StartNew(TaskMethod); /// <summary> /// 启动任务t3 /// </summary> Task t3 = new Task(TaskMethod); t3.Start();
连续任务
连续任务的特点是,连续任务的开启必要条件是上一个任务已经完成。也就是说:
如果一个任务t1的执行是依赖于另一个任务t2的,那么就需要在这个任务t2执行完毕后才开始执行t1。这个时候我们可以使用连续任务。
1 static void DoFirst(){ 2 Console.WriteLine("开始任务 : "+Task.CurrentId); 3 Thread.Sleep(3000); 4 } 5 static void DoSecond(Task t){//t为上一个任务 6 Console.WriteLine("任务"+t.Id+" finished.");//上一个任务已完成 7 Console.WriteLine("this task id is "+Task.CurrentId); 8 Thread.Sleep(3000); 9 } 10 static void Main(string[] args){ 11 Task t1 = new Task(DoFirst); 12 Task t2 = t1.ContinueWith(DoSecond); 13 }
任务的层次结构
我们在一个任务中启动一个新的任务,相当于新的任务是当前任务的子任务,两个任务异步执行,如果父任务执行完了但是子任务没有执行完,它的状态会设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态就变成RunToCompletion。