多线程项目中使用较少,每次都是从网上看一些简单的实例完成项目,并没有实际去深入了解相关的内部机制和原理,今天抽空梳理一下相关的知识点。
同步锁对比
lock和Monitor,在C#中lock是是Monitor的语法糖,本质上还是Monitor。Monitor能够对值类型进行加锁,实质上是Monitor.Enter(object)时对值类型装箱,而lock只能对对象进行操作。Monitor有TryEnter的功能,可以防止出现死锁的问题,lock没有。他们的作用都是为了保证多线程中只有一个线程访问代码段。
SpinLock 是一种低级互斥锁,可用于等待时间非常短的方案。
Mutex可以实现Monitor的功能,但Mutex是内核级别的,消耗较大的资源,不适合频繁的操作,会降低操作的效率。所以一般被调用部分的资源锁,常常用lock或者Monitor,可以提高效率。而线程和线程间的协调,可以用Mutex,因为相互互斥切换的机会会大大的降低,效率就不再那么的重要了。Mutex本身是可以系统级别的,所以是可以跨越进程的。比如我们要实现一个软件不能同时打开两次,那么Mutex是可以实现的,而lock和monitor是无法实现的。
Mutex只能互斥线程间的调用,但是不能互斥本线程的重复调用,把lock和Mutex结合起来使用就可以了。
多线程方法比较
线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形,对于执行长时间运行的计算密集型操作则会降低性能,并不适用。保持线程中的操作都是短暂的是非常重要的,不要在线程池中放入长时间运行的操作,或者阻塞工作线程。这将导致所有工作线程变得繁忙,从而无法服务用户操作。请注意线程池中的工作线程都是后台线程。这意味着当所有的前台线程(包括主程序线程)完成后,所有的后台线程将停止工作。
线程池中有两个很像的方法UnsafeQueueUserWorkItem和QueueUserWorkItem。他们实现的功能都一致,为了追求效率,UnsafeQueueUserWorkItem对堆栈里的内容信任,不再去遍历堆栈信息,读取权限,而是直接执行。
Task的背后使用的线程池技术进行实现的,但是它的性能要优于Thread Pool,因为它的使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的竞争减少。同时Task提供了丰富的API来管理线程控制。但是相对于前面的两种耗内存,Task依赖于CPU对于多核CPU性能远超前两者,单核的CPU三者的性能没有什么差别。
Parallel并行,让多个线程池线程并行工作。由于是并行执行,所以有一点需要注意:工作项彼此之间必须可以并行执行!Parallel.For效率高于Parallel.Foreach,所以当For与Foreach都可以时,推荐使用For。
Async/Await,IAsyncResult后续应用到了再做补充。
细节点
volatile多用于多线程的环境,当一个变量定义为volatile时,读取这个变量的值时候每次都是从momery里面读取而不是从线程的cache读。这样做是为了保证读取该变量的信息都是最新的。volatile主要是为了保障数据“读取”层面的时效性,而不是保证线程访问的操作顺序,因此它不是线程安全的。
不建议使用Abort方法终止线程,一方面是如果程序中使用了abortexception处理异常,其终止动作是可以恢复的,另外abort方法无法给出准确的终止时间,即调用abort方法后不是马上终止(例如线程正在执行非托管代码并没有返回)。终止线程的方法建议采用cancletoken。
前台线程:在主线程运行结束后,若前台线程没有运行完则会阻止主线程的关闭。后台线程:在主线程运行结束后,整个线程会结束。