线程Thread
- 线程是一个可执行路径,它可以独立于其他线程执行,每个线程都在操作系统的进程内执行,而操作系统提供了程序运行的独立环境
- 在C#中线程的创建方式是
var thread = new Thread(WriteY);
,他必须传入一个执行任务,这个执行任务可以是一个函数(委托),然后可以设置它的名字,调用Start()
来执行线程中的任务,在这里会看到X和Y交替输出,这是thread线程和主线程并发执行的效果。
1 | namespace ThreadDemo |
- 线程的一些属性
- IsAlive:线程一旦开始执行,IsAlive就是true,线程结束就为false,结束的条件是传入的委托结束了执行
- Name:线程的名称,只能设置一次,一般用于调试。
- CurrentThread:返回当前执行的线程
- 注意:线程一旦结束,就无法再重启
Join和Sleep
- Join:调用Join方法,就可以等待另一个线程结束,即thread2调用了Join方法后,其他的线程就会等待thread2执行结束后才能执行
1 | class Program |
- Sleep:会暂停当前的线程,但线程不会被抢占
- 添加超时:使用TimeSpan对象来设置Join的超时时间或设置Sleep的超时时间
1 | public class JoinTimeSpan |
- 注意:
Thread.Sleep(0)
这样调用会导致线程立即放弃当前的时间片,自动将cpu移交给其他线程。这个功能类似于Thread.Yield()
,但是它只会把执行交给同一处理器上的其他线程。- 当等待Sleep或Join的时候,线程处于阻塞的状态。
Sleep(0)
或Yield()
在高级性能调试中是一个很好的诊断工具,有助于发现线程安全问题,但在生产代码中不能随意地使用Yield()
函数。
阻塞
- 如果线程的执行由于某种原因导致暂停,则就认为该线程被阻塞了。被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此就不再消耗处理器的时间,直到满足其阻塞条件为止。
- 可以使用
ThreadState
属性来判断线程是否处于被阻塞状态,ThreadState
是一个枚举,可以用来获取线程的状态。它可以通过按位的形式合并状态的选项。
1 | class Program |
解除阻塞Unblocking
- 当遇到下列四种情况的时候,就会解除阻塞:
- 阻塞条件被满足
- 操作超时(需要设置超时条件)
- 通过
Thread.Interrupt()
进行打断 - 通过
Thread.Abort()
进行中止
上下文切换
- 当线程阻塞时或接触阻塞时,操作系统将执行上下文切换,这回产生少量开销。
阻塞vs忙等待
- 阻塞和忙等待也被称为
IO-bound
和Cpu-bound
- IO-bound操作的工作方式有两种:
- 在当前线程上同步等待
- 异步的操作
1 | //例如 |
- 而Cpu-bound就类似于死循环
1 | //例如 |
线程安全
本地状态和共享状态
- Local本地独立:CLR为每个线程分配自己的内存栈,以便使本地变量保持独立。
- Shared共享:
- 如果多个线程都引用到同一个对象实例,那么它们就共享了数据。
- 被lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段,所以也会被共享。
- 静态字段也会在线程间共享。
- 下面的_done字段就是共享变量
1 | class Program |
- 字段共享就会引出线程安全问题,因为上述例子的输出有可能不是固定的,如果在
_done = true;
前加上Thread.Sleep()
结果就会不一样
1 | class Program |
lock
- 在现实中应该尽量避免这种共享变量的使用,这种线程安全也可以用互斥锁lock来解决。
1 | class Program |
- 在这里使用
lock
将共享代码块包括,这个lock
代码块就称为临界区,每次只允许一个线程进入,在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全 - 但是使用lock来解决线程安全也存在很大的问题,第一很容易忘记对字段加锁,第二会引起死锁。
线程传参
- 可以直接使用lambda表达式做为Thread的参数,比如:
1 | class Program |
- 可以使用Thread的Start方法来传递任务的参数,因为传参委托的类型是
ThreadStart
,所以参数必须是object类型。
1 | class Program |
- 使用lambda表达式可以很简单地给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获地变量,比如下面的例子,每一次运行都有不同的输出,而且输出有可能会有相同的值。
1 | for (int i = 0; i < 10; i++) |
- 可以使用局部变量来解决这个问题
1 | for (int i = 0; i < 10; i++) |
线程的优先级
- 线程的优先级(Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
- 如果想让某线程的优先级比其他进程中的线程高,那就必须提升进程的优先级,这里可以使用Process类
1 | using (Process p = Process.GetCurrentProcess()) |
- 但是,提高线程或进程的优先级可能会导致其他线程或线程处于饥饿状态,不能随便设置。
信号
- 有时,你需要让某个线程一直处于等待状态,直到接收到其他线程发来的通知,才解除等待状态,这就叫做signaling。最简单的信号结构是
MaunalResetEvent
类对象。 - 调用
MaunalResetEvent
类对象的WaitOne
方法就会阻塞当前的线程,直到另一个线程通过调用Set
方法来打开信号(发送信号)。
1 | internal class Program |
- 信号打开后可以通过
Reset
方法来重新关闭信号。
同步上下文
- Thread Marshaling:Marshaling的意思是假如要将一个平台上的数据发送给另一个平台,但是两个平台使用的数据格式不一致,这时候就需要把数据转化为可发送的数据格式,这就类似于json的序列化,而接收端就是Unmarshaling,也就类似于反序列化。而Thread Marshaling就是把一些数据的所有权从一个线程交给另外一个线程。
- 在C#中同步上下文是用
System.ComponentModel
下的SynchronizationContext
抽象类来实现的,可以通过实例化SynchronizationContext
的子类,调用它的Post方法来实现同步上下文,这一般在富客户端应用比较常用(WPF、WinForm),它可以让主线程的一些事件的操作交给UI线程,这样UI线程可以做一下事件的调用,主线程也不会进入假死。
学习资料:B站杨旭