Thread
线程是一个可执行路径,可以独立于其他线程执行。
线程的属性
- IsAlive:线程一旦开始执行,IsAlive就是True,线程结束就变成了False。线程结束的条件,线程构造函数传入的委托方法执行结束。
- 线程一旦结束就无法再开启。
- Name:只能设置一次,以后再更改就会抛出异常。
- CurrentThread:返回当前执行的线程。
常用方法
- Join():等待当前线程执行完成后再开始后续的执行。可设置一个超时时间,执行超时返回False。
- Thread.Sleep():暂停当前线程,并等待指定的毫秒数。Thread.Sleep(0)会导致当前线程立即放弃当前的时间片,自动将cpu执行权移交给其他线程。
- Thread.Yield():把当前线程的执行权交给同一处理器上的其他线程
- Sleep(),Join()会让线程处于阻塞状态。
线程执行状态
阻塞(block):被阻塞的线程会立即将cpu的时间片生成给其他线程,从此不再消耗cpu时间,直到满足其阻塞条件为止才开始继续执行。
阻塞判断:
1
bool blocked = (thread1.ThreadState & ThreadState.WaitSleepJoin) != 0;
解除阻塞:
- 阻塞条件被满足
- 操作超时
- Thread.Interrput()
- Thread.Abort()
阻塞或接触阻塞,操作系统会执行上下文切换,会产生少量开销,通常为1或2微秒。
I/O-bound与Cpu-bound
I/O-bound:花费大部分时间等待某事发生的操作,例如输入输出,Thead.Sleep()。
Cpu-bound:花费大部分时间执行cpu密集型工作的操作。
线程安全
本地(Local)与共享(Shared)
Local:CLR为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立。
Shared:
多个线程引用同一个对象的实例,那么他们就共享了数据。
被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享。
静态字段(field)也会在线程间共享数据。
在读取和写入共享数据的时候,通过使用互斥锁(exclusive lock),来解决线程安全问题。
C#中使用lock语句来加锁,当两个线程同时竞争一个锁(锁可以基于任何引用对象)时,其中一个线程就会等待或阻塞,直到锁变成可用状态。
1 | class TestThreadSafe |
前台线程与后台线程
- 默认情况下,手动创建的线程就是前台线程。只要前台线程在运行,应用程序就还在运行;前台线程终止后,其余的后台线程也会全部终止,程序就会退出。
- 以在任何时候将前台线程修改为后台线程,方式是设置Thread.IsBackground = true。
信号
让线程一直处于等待状态,直到接受到其他线程发来的信号(signaling),才会继续执行。
1 | class Program |
线程池
- 不可以设置线程池的Name
- 池线程都是后台线程
- 阻塞池线程可使性能降级
- Thread.CurrentThread.IsThreadPoolThread 判断是否执行在池线程上
Task
Thread的问题
线程是用来创建并发的一种低级别工具,它有一些限制。尤其是:
- 虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程中获得返回值。
- 可能需要设置一些共享字段
- 如果操作抛出异常,捕获和传播该异常都很麻烦
- 无法告诉线程在结束时开始做另外的工作,必须进行Join操作(在进程中阻塞当前线程)。
- 很难使用较小的并发来组件大型的并发。
- 导致了对手动同步的更大依赖以及随之而来的问题。
Task类
Task类可以很好的解决Thread的问题。它代表了一个并发操作(可能由Thread支持,或不由Thread支持)。
- Task.Run()开启一个任务
- Task默认使用线程池,也就是后台线程。主线程结束,创建的所有Task都会结束
- Task.Run()返回一个Task对象,可以用来监视其过程
- Task.Wait()会阻塞当前线程,直到Task执行完成
默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Cpu-Bound类工作。
针对长时间运行的任务或者是阻塞操作,可以不采用线程池的方式来创建任务:
1 | class Program |
如果同时运行多个long-running tasks,尤其是由处于阻塞状态的,那么性能就会受到很大的影响,这时可采用:
- 如果任务是IO-Bound,等待一个在
async方法中返回Task或Task<T>的操作。 - 如果任务是Cpu-Bound,等待一个使用 Task.Run方法在后台线程启动的操作。
同步与异步
- 同步操作会在返回调用者之前完成它的工作
- 异步操作会在返回调用者之后去做它的(大部分)工作
- 异步方法会启用并发,因为它的工作会与调用者并行执行
- 异步方法通常很快就会返回到调用者,所以叫非阻塞方法
- 异步不会提升单个应用程序的运行速度,但是能提升服务器的并发访问量。
async
- 用
async关键字修饰的方法称为异步方法。 - 异步方法的返回值一般是
Task<T>,T是真正的返回值类型。惯例:异步方法名称一般以Async结尾。 - 即使异步方法没有返回值,也最好声明为非泛型的
Task。 - 调用泛型方法时,一般在方法前加上
await关键字,这样拿到的返回值直接就是泛型指定的T类型。 - 一个方法使用
await关键字后,这个方法必须声明使用async修饰。
await
await关键字简化了附加continuation的过程
await关键字后面expression会马上返回,直接执行expression后续的语句,expression执行完成后自动回调。
await调用的等待期间,.NET会把当前的线程返还给线程池,等异步方法调用执行完毕后,再从线程池中取出一个线程执行后续的代码。
Task 与async Task
Task:在方法前面加Task或是Task<T>,就是表明方法是可等待的。也是C#中的一种Task异步编程模式,结合await可方便的进行异步并发编程。
async Task:表明方法就是异步方法。