求知若饥,虚心若愚
题外知识,协程和线程
线程同步的意义
- 多个线程访问同个数据可能会产生竞态条件,从而获取意想之外的结果。原因在于不同线程获取的是拷贝值,需要同步,如果同步中间出现了不得知的修改(即非原子性)就会破坏数据完整性
- 对可变的静态对象进行同步(永远不变的东西不需要同步),因为同步是以牺牲性能为代价
使用Monitor同步
- 原理是阻止第二个线程进入受保护的代码段,直到第一个线程退出那个代码段。
- 使用Monitor.Enter()个Monitor.Exit()来保护,记住所有代码都要用try/finally语句,如果发生异常导致没有Exit,则会无限阻塞
- Monitor还有Pulse方法,运行线程进入“就绪队列”,指出下一个就到它运行(获得锁),用于实现生产者-消费者模型
1 | using System; |
使用lock关键字
避免使用Monitor时忘记try/finally,但其实lock就是调用了Monitor
1 | ··· |
lock对象的选择
- 不能用值类型,因为会装箱,导致两者没有关联
- 上例声明为只读是避免Enter和Exit直接修改了该对象
- 同样尽量设为私有,避免其他去修改(除非要同步的数据也是公共)
- 不要锁定this、typeof()或者字符串(字符串常量可能会出现多个地方)
将字段声明为volatile
强迫编译器和“运行时”对该修饰字段的所有读写操作都在代码指示位置发生(因为默认可能会对代码优化,打乱顺序或者拿掉无用指令)。但一般不用这个,用lock会更好,除非特别熟悉。
使用System.Threadind.Interlocked类
使用Interlocked类提供的一些操作是线程安全的,而且不用使用lock来大幅降低性能,但他只能处理常见的简单同步问题。
同步设计的最佳实践
- 避免死锁,不要以不同顺序请求相同两个或更多同步目标的排他所有权,要确保同时持有多个锁的代码总是相同顺序获得这些锁
- 静态数据都应该是线程安全的(通过公用方法来修改私有静态变量并提供同步机制)。实例数据不需要同步机制,除了显式设计成由多个线程访问的类
- 避免不必要的锁定,比如小于本机指针大小的值的读写(本身就是原子的)
更多同步类型
- System.Threading.Mutex,可限制应用程序不能运行多个实例和多个进程之间同步
- WaitHandle
- 重置事件类ManualResetEvent|ManualResetEventSlim,重置事件用于强迫代码等候另一个线程的执行,直到获得事件已发生的通知。
- Semaphore|SemaphoreSlim|CountdownEvent,都是计数机制,满足条件后访问
- 并发集合类BlockingCollection
、ConcurrentBag 、ConcurrentDictionary<TKey, TValue>、ConcurrentQueue 、ConcurrentStack
线程本地存储
都是每个线程存储一份自己本地的内存,互不干扰
- ThreadLocal
- ThreadStaticAttribute,初始化好像只会在主线程初始化
计时器
使用async(调用之后执行的代码会在支持的线程上下文继续,能避免UI跨线程问题)/await和Task.Delay实现计时器
1 | using System; |
使用内置类实现(需要注意UI跨线程问题)
- UI线程友好:System.Windows.Forms.Timer、System.Windows.Threading.DispatcherTimer、System.Timers.Timer(要专门配置)
- 性能友好:System.Threading.Timer