四、多线程
4.1 线程与线程函数
线程是操作系统分配CPU的基本单元。应用程序把要完成的数据处理任务分为多份,把分割出来的工作任务封装为一个函数由线程负责执行。

线程的运行过程体现为线程函数的运行过程。
4.2 线程分类
- 前台线程
默认情况下,所有创建的线程其IsBackground属性为False,这种线程称为前台线程。CLR会等待所有前台线程结束后才会结束整个进程。 - 后台线程
IsBackground属性为True的线程被称为后台线程。主线程结束执行时,后台线程会被自动中断执行。
4.3 线程的使用
4.3.1 线程创建
线程由Thread类创建的对象代表。

满足这一委托的实例或静态方法都可以作为线程函数。

4.3.2 线程启动
当线程对象创建以后,调用它的Start()方法进行启动,当线程函数代码执行完毕,线程自然结束。
- 通常情况下,程序中的第一个启动的线程作为主线程,由它负责启动其它线程。
- 在线程函数中使用
Thread.CurrentThead可获取负责执行此函数的线程对象的引用。 - 每个线程有一个ID,Thread对象的
ManagedThreadId属性保存了这个值。
4.3.3 线程阻塞(暂停/休眠)
Thread.Sleep()方法让线程进入阻塞状态。

Thread.SpinSleep()方法让CPU空转,但线程状态不改变。

4.3.4 线程中止
调用主线程的Abort()方法提前终止线程。
注意:需要在线程函数体内捕获ThreadAbortException异常。
4.3.5 线程的Join
Thread类的Join方法可以让一个线程等待另一个线程的结束。

在线程A的执行流程中调用线程B的Join()方法,其实是相当于把线程B的整个执行流程嵌入到线程A的执行流程中。线程A会在调用Join()方法的那句代码处等待,直到线程B工作完成。
4.4 线程状态
4.4.1 线程状态(ThreadState)

4.4.2 线程状态转换

4.5 多线程编程技巧
4.5.1 在线程函数中调用有参数和返回值的函数
方法一:外套函数


- 优点:简单直观
- 缺点:
- 字段公有违背面向对象封装原则;
- 如果有多个方法,则字段数目产生爆炸式增长。
方法二:使用ParameterizedThreadStart委托
- 带参数的ParameterizedThreadStart委托声明
public delegate void ParameterizedThreadStart(Object obj) - 将要传递给线程函数的信息封装为一个对象,然后调用Tread类的构造函数
public Thread (ParameterizedThreadStart start) - 启动线程时,线程传递一个参数信息对象即可
Thread t=new Thread(new ParameterizedThreadStart(线程函数));
t.Start(封装的线程参数对象);
4.5.2 应用示例
- 单参无返回值函数示例

只有一个参数、返回void的方法,直接吧它的参数类型改为Object即可。


- 多参有返回值的函数示例
通过创建参数辅助类的方式将其进行包装改造,使其满足要求,也可以使用委托达到回调的目的。




4.6 UI线程
4.6.1 UI线程简介
在多线程程序中不允许跨线程直接访问可视化控件。创建可视化对象的线程叫UI线程,它拥有一个消息循环和消息队列。真正具备响应消息能力的是UI线程函数启动的消息循环,由它负责分发消息和调用窗体过程。


窗体负责响应消息的函数称为窗体过程(Windows Procedure)

用户的操作被转换为消息放入系统消息队列。操作系统有一个专门的线程负责从这一队列中取出消息,根据消息的目标对象(窗体句柄),将其移动到创建它的UI线程所对应的消息队列中。

4.6.2 实现跨线程访问可视化控件的方法
- 编程原则
对于UI控件只允许创建它的线程访问。 - 技术要点
工作线程将希望UI控件显示的信息转换为消息,将其插入到UI线程的消息队列中。 - 实现方式
- 使用Control类的Invoke方法。

- 示例

- 向跨线程访问控件的函数传递参数

4.7 BackgroundWorkder组件
BackgroundWorkder组件非常适合于在后台运行任务、前台显示结果的应用场景,而且提供了完善的取消工作、报告进度和异常处理的功能,解决了跨线程访问可视化组件的问题。因此,在编写拥有可视化界面的多线程程序时,应优先选择BackgroundWorkder组件。
4.7.1 BackgroundWorkder组件使用说明
- DoWork事件
处理业务逻辑代码都写在BackgroundWorkder组件的DoWork事件响应函数中。 - 启动工作任务
调用BackgroundWorkder组件的RunWorkerAsync()方法启动工作任务,此方法会触发DoWork事件并自动调用其事件响应方法。

- 信息传递
调用RunWorkerAsync()方法带参重载形式将信息传送给BackgroundWorkder组件
在public void RunWorkerAsync(Object argument)DoWork事件响应方法的参数e中可以通过e.Argument属性获取外界传入的信息。 - 取回结果
工作任务结束后BackgroundWorkder组件激发RunWorkerCompleted事件,在此事件的参数e(为RunWorkerCompletedEventArgs类型对象)可以取回工作结果。e.Result:工作任务执行的结果。e.Error:这是一个Exception对象,如果工作任务执行过程中没发生一次,则此属性为null,如果发生了异常,e.Error就是被抛出的异常。e.Cancelled:如果在工作任务完成前用户取消了操作,则此属性为True,否则此属性为False。
- 取消工作任务
BackgroundWorkder组件有一个CancelAsync()方法,调用此方法将会导致BackgroundWorkder组件的制度属性CancellationPending为True。在DoWork事件处理代码中就可以检查此属性值以取消工作任务。

- 报告工作进度
在DoWork事件响应代码合适的地方调用BackgroundWorkder组件的ReportProgress方法,就会激发ProgressChanged事件,并且调用ReportProgress方法说设置的参数(包括当前工作完成的百分比和其它附加信息)会被传送到ProgressChanged事件的事件参数中。

编写ProgressChanged事件相应代码,从ProgressChanged事件参数中取出DoWork事件中传出的信息。
4.7.2 总结
- 在
DoWork事件中编写代码完成要以多线程方式进行的工作。 - 调用
RunWorkerAsync()方法启动多线程执行,此方法会同事激发DoWork事件。 - 响应
RunWorkerCompleted事件取回工作结果。 - 调用
CancelAsync()方法设置取消标记属性CancellationPending=True,然后在DoWork事件中取消工作任务。 - 在
ProgressChanged事件中访问UI窗体上的控件,以向外界报告工作进度。 - 当工作处理完成会自动触发
RunWorkerCompleted事件,在此方法向用报告工作任务结束。
4.8 线程同步问题
4.8.1 线程死锁
死锁表示系统进入一个僵持状态,所有线程都没有执行完毕,但却谁都没办法继续执行。
- 线程交叉等待造成死锁

- 共享资源访问不当造成死锁

所谓共享资源指多个线程可以同时访问的数据结构、文件等信息实体。
4.8.2 多个线程数据存取错误
当多个线程访问同一个数据时,如果不对读和写的顺序做出限定,例如一个线程正在读数据,而另一个线程尝试写数据,则读数据的线程得到的数据为脏数据也可能出错。
4.9 线程同步方法
锁(lock)主要用于同步多个线程对共享资源的访问,也可用于控制各个线程之间的推进顺序。
4.9.1 使用Monitor对象实现锁

Enter与Exit方法必须严格配对,否则可能出现死锁情况。Monitor一般只用于访问引用类型的共享资源,不要将Monitor用于值类型的共享资源!
4.9.2 lock

lock等价于Monitor

使用lock编写线程安全的方法示例:

4.9.3 等待句柄(WaitHandle)
4.9.3.1 WaitHandle
WaitHandle是抽象类,在实际开发中常用的是它的子类。

//当线程调用某个WaitHandle对象的WaitOne()方法时,如果此对象处于signaled状态,线程可以继续执行,否则线程被阻塞直到其它某个线程将WaitHandle对象置为signaled状态为止。
public virtual bool WaitOne();
//线程可调用WaitHandle类的WaitAll()方法等待多个WaitHandle对象都变成signaled状态。
public static bool WaitAll(WaitHandle[] WaitHandles);
//线程还可调用WaitHandle类的WaitAny()方法等待一组WaitHandle对象中的任一个变为signaled状态。
4.9.3.2 Mutex
Mutex适用于多个线程互斥访问同一个共享资源。

使用方式:
- 一个线程要访问共享资源必须调用
Mutex对象的WaitXXX()系列方法提出申请。 - 当申请得到批准的线程完成了对于共享资源的访问后,它调用Mutex对象的
ReleaseMutex()方法释放对于共享资源的访问权。

4.9.3.3 Semaphore
当有多个资源需要同步访问时可以使用Semaphore,Semaphore对象一次可以使用多个线程投入运行。

使用原理:
Semaphore类内部维护一个计数器- 当一个线程调用
Semaphore对象的Wait系统方法时,此计数器减一,只要计数器还是一个正数线程就不会阻塞。 - 当计数器减到0时,再调用
Semaphore对象WaitXXX()系列方法的线程将被阻塞。 - 直到有线程调用
Semaphore对象的Release()方法增加计数器值,才有可能解除阻塞状态。
4.9.3.4 EventWaitHandle
在一个多线程程序中,如果多个线程在等待绿灯,绿灯一亮这些线程都可以投入运行,红灯一亮所有线程又被阻塞。想要实现这种多个线程走走停停的功能可以使用线程同步事件类EventWaitHandle来实现。

EventWaitHandle类派生自WaitHandle。Set()方法将自身设置为signaled状态,相当于绿灯亮。Reset()方法将自身设置为non-signaled状态,相当于红灯亮。
ManualResetEvent:一次允许多个线程投入运行。
AutoResetEvent:一次仅运行一个线程投入运行。