C#特性知识图谱四、多线程

四、多线程

4.1 线程与线程函数

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

4.2 线程分类

  • 前台线程
    默认情况下,所有创建的线程其IsBackground属性为False,这种线程称为前台线程。CLR会等待所有前台线程结束后才会结束整个进程。
  • 后台线程
    IsBackground属性为True的线程被称为后台线程。主线程结束执行时,后台线程会被自动中断执行。

4.3 线程的使用

4.3.1 线程创建

线程由Thread类创建的对象代表。
image
满足这一委托的实例或静态方法都可以作为线程函数。
image

4.3.2 线程启动

当线程对象创建以后,调用它的Start()方法进行启动,当线程函数代码执行完毕,线程自然结束。

  • 通常情况下,程序中的第一个启动的线程作为主线程,由它负责启动其它线程。
  • 在线程函数中使用Thread.CurrentThead可获取负责执行此函数的线程对象的引用。
  • 每个线程有一个ID,Thread对象的ManagedThreadId属性保存了这个值。

4.3.3 线程阻塞(暂停/休眠)

Thread.Sleep()方法让线程进入阻塞状态。
image
Thread.SpinSleep()方法让CPU空转,但线程状态不改变。
image

4.3.4 线程中止

调用主线程的Abort()方法提前终止线程。
注意:需要在线程函数体内捕获ThreadAbortException异常。

4.3.5 线程的Join

Thread类的Join方法可以让一个线程等待另一个线程的结束。
image
在线程A的执行流程中调用线程B的Join()方法,其实是相当于把线程B的整个执行流程嵌入到线程A的执行流程中。线程A会在调用Join()方法的那句代码处等待,直到线程B工作完成。

4.4 线程状态

4.4.1 线程状态(ThreadState)

image

4.4.2 线程状态转换

image

4.5 多线程编程技巧

4.5.1 在线程函数中调用有参数和返回值的函数

方法一:外套函数
image
image

  • 优点:简单直观
  • 缺点:
    1. 字段公有违背面向对象封装原则;
    2. 如果有多个方法,则字段数目产生爆炸式增长。

方法二:使用ParameterizedThreadStart委托

  • 带参数的ParameterizedThreadStart委托声明
    public delegate void ParameterizedThreadStart(Object obj)
  • 将要传递给线程函数的信息封装为一个对象,然后调用Tread类的构造函数
    public Thread (ParameterizedThreadStart start)
  • 启动线程时,线程传递一个参数信息对象即可
    Thread t=new Thread(new ParameterizedThreadStart(线程函数));
    t.Start(封装的线程参数对象);

4.5.2 应用示例

  • 单参无返回值函数示例
    image
    只有一个参数、返回void的方法,直接吧它的参数类型改为Object即可。
    image
    image
  • 多参有返回值的函数示例
    通过创建参数辅助类的方式将其进行包装改造,使其满足要求,也可以使用委托达到回调的目的。
    image
    image
    image
    image

4.6 UI线程

4.6.1 UI线程简介

在多线程程序中不允许跨线程直接访问可视化控件。创建可视化对象的线程叫UI线程,它拥有一个消息循环和消息队列。真正具备响应消息能力的是UI线程函数启动的消息循环,由它负责分发消息和调用窗体过程。
image
image
窗体负责响应消息的函数称为窗体过程(Windows Procedure)
image
用户的操作被转换为消息放入系统消息队列。操作系统有一个专门的线程负责从这一队列中取出消息,根据消息的目标对象(窗体句柄),将其移动到创建它的UI线程所对应的消息队列中。
image

4.6.2 实现跨线程访问可视化控件的方法

  • 编程原则
    对于UI控件只允许创建它的线程访问。
  • 技术要点
    工作线程将希望UI控件显示的信息转换为消息,将其插入到UI线程的消息队列中。
  • 实现方式
  1. 使用Control类的Invoke方法。
    image
  • 示例
    image
  1. 向跨线程访问控件的函数传递参数
    image

4.7 BackgroundWorkder组件

BackgroundWorkder组件非常适合于在后台运行任务、前台显示结果的应用场景,而且提供了完善的取消工作、报告进度和异常处理的功能,解决了跨线程访问可视化组件的问题。因此,在编写拥有可视化界面的多线程程序时,应优先选择BackgroundWorkder组件。

4.7.1 BackgroundWorkder组件使用说明

  • DoWork事件
    处理业务逻辑代码都写在BackgroundWorkder组件的DoWork事件响应函数中。
  • 启动工作任务
    调用BackgroundWorkder组件的RunWorkerAsync()方法启动工作任务,此方法会触发DoWork事件并自动调用其事件响应方法。
    image
  • 信息传递
    调用RunWorkerAsync()方法带参重载形式将信息传送给BackgroundWorkder组件
    public void RunWorkerAsync(Object argument)
    
    DoWork事件响应方法的参数e中可以通过e.Argument属性获取外界传入的信息。
  • 取回结果
    工作任务结束后BackgroundWorkder组件激发RunWorkerCompleted事件,在此事件的参数e(为RunWorkerCompletedEventArgs类型对象)可以取回工作结果。
    1. e.Result:工作任务执行的结果。
    2. e.Error:这是一个Exception对象,如果工作任务执行过程中没发生一次,则此属性为null,如果发生了异常,e.Error就是被抛出的异常。
    3. e.Cancelled:如果在工作任务完成前用户取消了操作,则此属性为True,否则此属性为False。
  • 取消工作任务
    BackgroundWorkder组件有一个CancelAsync()方法,调用此方法将会导致BackgroundWorkder组件的制度属性CancellationPending为True。在DoWork事件处理代码中就可以检查此属性值以取消工作任务。
    image
  • 报告工作进度
    DoWork事件响应代码合适的地方调用BackgroundWorkder组件的ReportProgress方法,就会激发ProgressChanged事件,并且调用ReportProgress方法说设置的参数(包括当前工作完成的百分比和其它附加信息)会被传送到ProgressChanged事件的事件参数中。
    image
    编写ProgressChanged事件相应代码,从ProgressChanged事件参数中取出DoWork事件中传出的信息。

4.7.2 总结

  • DoWork事件中编写代码完成要以多线程方式进行的工作。
  • 调用RunWorkerAsync()方法启动多线程执行,此方法会同事激发DoWork事件。
  • 响应RunWorkerCompleted事件取回工作结果。
  • 调用CancelAsync()方法设置取消标记属性CancellationPending=True,然后在DoWork事件中取消工作任务。
  • ProgressChanged事件中访问UI窗体上的控件,以向外界报告工作进度。
  • 当工作处理完成会自动触发RunWorkerCompleted事件,在此方法向用报告工作任务结束。

4.8 线程同步问题

4.8.1 线程死锁

死锁表示系统进入一个僵持状态,所有线程都没有执行完毕,但却谁都没办法继续执行。

  • 线程交叉等待造成死锁
    image
  • 共享资源访问不当造成死锁
    image

所谓共享资源指多个线程可以同时访问的数据结构、文件等信息实体。

4.8.2 多个线程数据存取错误

当多个线程访问同一个数据时,如果不对读和写的顺序做出限定,例如一个线程正在读数据,而另一个线程尝试写数据,则读数据的线程得到的数据为脏数据也可能出错。

4.9 线程同步方法

锁(lock)主要用于同步多个线程对共享资源的访问,也可用于控制各个线程之间的推进顺序。

4.9.1 使用Monitor对象实现锁

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

4.9.2 lock

image
lock等价于Monitor
image
使用lock编写线程安全的方法示例:
image

4.9.3 等待句柄(WaitHandle)

4.9.3.1 WaitHandle

WaitHandle是抽象类,在实际开发中常用的是它的子类。
image

//当线程调用某个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适用于多个线程互斥访问同一个共享资源。
image
使用方式:

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

image

4.9.3.3 Semaphore

当有多个资源需要同步访问时可以使用SemaphoreSemaphore对象一次可以使用多个线程投入运行。
image
使用原理:

  • Semaphore类内部维护一个计数器
  • 当一个线程调用Semaphore对象的Wait系统方法时,此计数器减一,只要计数器还是一个正数线程就不会阻塞。
  • 当计数器减到0时,再调用Semaphore对象WaitXXX()系列方法的线程将被阻塞。
  • 直到有线程调用Semaphore对象的Release()方法增加计数器值,才有可能解除阻塞状态。

4.9.3.4 EventWaitHandle

在一个多线程程序中,如果多个线程在等待绿灯,绿灯一亮这些线程都可以投入运行,红灯一亮所有线程又被阻塞。想要实现这种多个线程走走停停的功能可以使用线程同步事件类EventWaitHandle来实现。
image

  • EventWaitHandle类派生自WaitHandle
  • Set()方法将自身设置为signaled状态,相当于绿灯亮。
  • Reset()方法将自身设置为non-signaled状态,相当于红灯亮。

ManualResetEvent:一次允许多个线程投入运行。
AutoResetEvent:一次仅运行一个线程投入运行。

原文地址:https://www.cnblogs.com/tjubuntu/p/15438624.html