简单解释:单个CPU一次只能运行一个进程,某一时刻CPU总是运行一个进程,其他进程处于非运行状态,一个进程包含着多个线程,这些线程协同共同完成一个任务,进程之间的内存是共享的.
一.开启线程的方法:
1.通过委托开启(异步委托):
通过委托调用BeginInvoke()方法来开启一个委托.其返回一个IAsyncResult对象,这个对象记录了当前线程的状态,如:线程是否执行完毕等. 调用EndInvoke()方法获取这个委托引用的方法的返回值,参数为IAsyncResult对象.
2.通过Thread类开启:
创建一个Thread类的对象,这个类的构造方法的参数是一个委托,这个委托只能指向参数类型是空或者object类型的参数
如:Thread t = new Thread(DownloadFile);调用Thread对象的Start()方法开启线程(当有参数时参数在Start()方法中传递)当要传递多个参数时应将参数封装在一个类中.
二.线程结束检测:
方法 | 说明 | 缺点/优点 |
---|---|---|
使用一个死循环判断委托返回的IAsyncResult对象的IsCompleted属性,true为已经结束 | IAsyncResult对象的IsCompleted属性记录了线程结束与否,判断这个属性就能知道线程是否结束 | 方法太过繁琐而且主线程会陷入死循环 |
使用IAsyncResult的AsyncWaitHandle属性的WaitOne()方法,该方法传递一个超时时间,当等待时间超过超时时间时返回一个bool值表示线程结束与否 | 线程执行超时的时间后不管有没有结束都结束线程 | 优点:可以控线程的执行时间.缺点:需要估计线程的结束时间. |
使用BeginInvoke()的后两个参数,//倒数第二个参数是一个委托(参数是syncResult),表示回调函数,就是当线程结束时调用的方法,倒数第一个参数用来给回调函数传递数据(是一个object的对象) | 线程结束时会自动调用回调方法,自动执行线程结束后要执行的方法 | 优点:不用去预估线程结束的时间.缺点:不管线程会执行多久都会等下去 |
三、前台线程和后台线程:
只有一个前台线程在运行,应用程序的进程就在运行,就算Main方法结束了,应用程序的进程仍然是运行的,直到所有的前台线程完成其任务为止(只要有前台线程这个程序就没有停止)。默认情况下,用Thread类创建的线程都是前台线程。线程池的其他线程总是后台线程。在用Thread类创建新线程的时候,可以设置IsBacnground属性,表示它是一个前台线程还是一个后台线程。当前台线程运行完毕后,如果还有后台线程的话,所有后台线程会被终止。
四、线程的优先级
线程由操作系统调度,一个CPU同一时间只能做一件事情,当有很多线程需要CPU去执行时,线程的调度器会根据线程的优先级去判断先去执行哪一个线程,如果优先级相同的话就使用一个循环调度规则,逐个执行每个线程。在Thread类中,可以设置Priority属性,一影响线程的优先级,Priority属性是ThreadPriority枚举定义的一个值。定义的级别有Highest,AboveNormal,BelowNormal和Lowst.
五、线程状态:假设t1,t2是两个线程
线程状态 | 说明 |
---|---|
初始状态(Unstarted) | 当使用Thread类或委托开启一个线程,这个线程就进入了初始状态.线程还未启动 |
Running状态(运行) | 线程已经调用Start()方法,代表线程已经启动正在运行 |
WaiSleepJoin状态(阻塞) | 线程调用Sleep()方法、Join()方法。 |
Suspended状态(挂起) | 线程挂起 |
Stopped状态(终止) | 处于这个状态的线程将不复存在 |
方法 | 调用方式 | 说明 |
---|---|---|
Thread.Start() | t1.Start() | 开启线程,t1线程进入Running状态 |
Thread.Suspend() | t1.Suspend() | 挂起线程,或者如果线程已挂起,则不起作用;这意味着线程将不再得到CPU时间线程先进入SuspendRequested(申请挂起)状态,再进入Suspended状态 |
Thread.Resume() | t1.Resume() | 继续以挂起的线程,线程由Suspended状态进入Running状态 |
Thread.Sleep() | t1.Sleep(5000) | t1线程进入WaiSleepJoin状态(阻塞状态),阻塞5000毫秒。 |
Thread.Join() | t1.Join()在t2线程中调用 | t2将会被阻塞,直到ti线程终止。 |
Thread.Interrupt() | t1.Interrupt() | 只可以中断处于 WaitSleepJoin 状态的线程(即线程t1将不再往下执行),当线程状态不再为 WaitSleepJoin时,线程将恢复执行。 |
Thread.Abort() | t1.About() | 终止线程t1,并且不可恢复。 |
六、线程池 (后台线程)
当使用委托类型开启线程时CLR并不会创建新的线程,为了获取更高的效率,委托创建了由运行时维护的工作者线程池。C#提供了ThreadPool管理线程池。这个类会在需要的时候增减线程池中的线程数,直到达到最大线程数。池中的最大线程数是可配置的。在双核CPU中默认设置为1023个工作线程和1000个I/O线程。
可以用ThreadPool.QueueUserWorkItem()方法使用线程池中的线程来执行一个方法,这个方法的参数是一个WaiCallbaack委托,这个委托指向一个参数为一个object对象的方法。
!!!注意线程池中的线程均为后台线程,并且不能改为前台线程 。不能给线程池中的线程设置优先级或者名称。线程池中的线程只能运行一些时间较短的任务。
七、任务(Task类)
创建方式1.新建Tast对象(传递一个委托),调用Task对象的Start()方法开启任务
创建方式2.新建一个TaskFactory的对象,调用TaskFactory对象的StartNew()方法开启任务,该方法的参数是一个委托,该方法返回一个Tast对象。
八、连续任务
如果一个任务t1的执行是依赖于另一个任务t2的,那么就需要这个任务t2执行后才会开始执行t1,这个时候我们可以使用连续任务。如下所示,t2会在t1执行完毕后才开始执行:t2=t1.ContinueWith(DoSecond)。
九、任务的层次结构
在一个任务中开启一个新的任务,相当于引得任务是当前任务的子状态,两个任务异步执行,如果父任务执行完了,子任务还没执行完,当前任务的状态会设置为WaitingForChildrenToComplete状态,只有当子任务执行完,父任务才会被设置为RunToCompletion状态(父任务会等带子任务执行)
十、线程问题
1、争用条件:多个线程同时去改变同一个变量可能导致数据混乱(多个线程争抢资源可能导致数据混乱。)如下所示:线程t和线程t1同时去调用对象中的方法,这会造成争用问题导致数据混乱。
MyThreadObject m = new MyThreadObject();
Thread t = new Thread(ChangeState);
t.Start(m);
Thread t1=new Thread(ChangeState);
t1.Start(m);
Console.ReadKey();
2、线程锁:线程锁用来解决线程争用问题,如上所诉,当多个线程同时去执行同一个方法时就会造成线程争用,解决方法就是给这个方法加上线程锁表明这个方法有线程在调用让别的线程先暂停。如下所示:(lock的作用是锁定某一代码块,让同一时间只有一个线程访问该代码块)
static void ChangeState(Object o)
{
MyThreadObject m = o as MyThreadObject;
while (true)
{
//向系统申请锁定m对象,如果m对象没有被锁定就可以给m对象加锁
//如果m对象已经被锁定那么这个语句就会暂停直到m对象解锁
lock (m)
{
m.CahangeState();//在同一时刻只有一个线程在执行这个方法
}//释放锁定
}
}