《Inside C#》笔记(十三) 多线程 下
一 任务调度
当一个线程的时间片被用尽后,处理器会切换到另一个线程,但关于如何确定执行哪一个线程呢,这就涉及到了线程或任务的优先级。
a) 每个线程都有优先级,任务调度算法会根据各线程的不同优先级来决定出下一个执行的线程。线程的优先级分为Highest、AboveNormal、Normal、BelowNormal、Lowest,线程的默认优先级为Normal。可以通过Thread.Priority属性来调整优先级。
b) 只能设置上述5个等级,但实际上英特尔处理器往往有32个等级的优先级(0-31),我们设置的个5个等级中的一个会被作为调度算法的一项考虑因素,算法还会综合考虑线程的优先级以及其它一些动态因素,来确定最终的优先级。0-31中,值越大优先级越高。
c) 设置任务优先级时要留心,轻易不要将一个任务的优先级设定为Highest。例如,有一个GUI线程A,背后还有一个负责相关处理工作的线程B,两个线程同时执行,界面不会卡死。但如果将线程B的优先级设置地太高,GUI线程就可能因得到的时间片太少而出现卡顿。一般情况下,建议将形成的优先级保留为默认的Normal级别,这样系统会轮询调度各个线程(round robin scheduling)。
二 线程安全与同步
在单线程环境下,在执行到代码的某些地方时,有些对象会因未初始化、失去引用等而出现临时的不可用状态,这在单线程中是正常的。但如果这样的代码被多个线程执行,情况就会变得复杂起来,且难以预测,一个刚好处于不可用状态的对象可能会被另一个线程使用。“线程安全”(thread safety)指的便是对象在被使用时始终处在有效的状态下。
那么怎么保证线程安全并尽可能地消除多线程带来的不确定性呢。常用的方法为:同步。这可以确保某个代码块一次只能被一个线程访问,当一个线程使某个对象临时不可用时,其它的线程根本无法执行相关的代码。
a) System.Monitor
可以使用Monitor.Enter()和Monitor.Exit()方法将需要同步的代码包裹起来,这样当一个线程执行到这个区域时,会尝试取得当前对象的monitor lock,如果monitor lock已经被占用,会等到锁被释放后再去锁定相关区域。
b) lock
lock的使用方式如下:
效果与Monitor的相关方法类似,但因为有大括号,不需要专门指定的解锁代码。
c) Mutex
System.Threading.Mutex(互斥锁)相比前面的两种锁速度会较慢,但更加灵活。
Mutex有如下三个构造函数:
使用第一个构造函数创建的互斥锁属于当前的线程,只能被当前线程使用;第二个构造函数通过布尔值来指定当前线程是否为锁的拥有者。第三个在第二个的基础上,还能设置锁的名称。基本用法为:
mutex.WaitOne方法也有多个重载:
后两个不会让别的线程无条件地等待。
d) 在.NET类库中,不是所有的System.***的类都是线程安全的,没有必要这么做,而且会影响性能。比如如果为一个集合的Add方法上锁,当数据量较大时插入操作会异常缓慢。
三 多线程使用指南
了解什么时候什么场合适合使用多线程,可以避免让多线程编程成为一场灾难。
a) 何时可以使用多线程
在高并发场合、系统的结构比较清晰简单、为了有效利用处理器资源时,可以考虑多线程。
为了简化系统的一个有效方法便是使用队列和同步机制。使用队列、而不是直接调用系统的方法,可以让相关对象顺序进入队列,然后由服务端多线程地处理队列中的内容。这样的系统会更可靠、更健壮、伸缩性也更好。
b) 何时不应该使用多线程
滥用多线程还不如不用。当使用多线程的成本大于收益时、但你尚未彻底搞清楚系统的各种可能时、但你想不出使用多线程的必要理由时,便不应使用多线程。
学习资料:Inside C# by Tom Archer