求知若饥,虚心若愚
题外知识,协程和线程
线程同步的意义
多个线程访问同个数据可能会产生竞态条件,从而获取意想之外的结果。原因在于不同线程获取的是拷贝值,需要同步,如果同步中间出现了不得知的修改(即非原子性)就会破坏数据完整性
对可变的静态对象进行同步(永远不变的东西不需要同步),因为同步是以牺牲性能为代价
使用Monitor同步
原理是阻止第二个线程进入受保护的代码段,直到第一个线程退出那个代码段。
使用Monitor.Enter()个Monitor.Exit()来保护,记住所有代码都要用try/finally语句,如果发生异常导致没有Exit,则会无限阻塞
Monitor还有Pulse方法,运行线程进入“就绪队列”,指出下一个就到它运行(获得锁),用于实现生产者-消费者模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using System; using System.Threading; using System.Threading.Tasks; class Program { // 通过共享作为参数传递的同一个对象引用来关联 readonly static Object _Sysc = new Object(); const int _Total = int.MaxValue; static long _Count = 0; public static void Main() { Task task = Task.Run(() => Decrement()); for (int i = 0; i < _Total; i++) { bool lockToken = false; try { Monitor.Enter(_Sysc, ref lockToken); _Count++; } finally { if (lockToken) { Monitor.Exit(_Sysc); } } } task.Wait(); System.Console.WriteLine($"Count = {_Count}"); } private static void Decrement() { for (int i = 0; i < _Total; i++) { bool lockToken = false; try { Monitor.Enter(_Sysc, ref lockToken); _Count--; } finally { if (lockToken) { Monitor.Exit(_Sysc); } } } } }
使用lock关键字
避免使用Monitor时忘记try/finally,但其实lock就是调用了Monitor
1 2 3 4 5 ··· lock(_Sync) { _Count++; }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 using System; using System.Threading.Tasks; class Program { static async Task Main() { await TickAsync(new CancellationToken()); } private static async Task TickAsync(System.Threading.CancellationToken token) { for (int minute = 0; minute <= 24; minute++) { DisplayMinuteTicker(minute); for (int second = 0; second < 60; second++) { await Task.Delay(1000); if(token.IsCancellationRequested) break; DisplaySecondTicker(); } if(token.IsCancellationRequested) break; } } private static void DisplaySecondTicker() { System.Console.WriteLine("DisplaySecondTicker"); } private static void DisplayMinuteTicker(int minute) { System.Console.WriteLine("DisplayMinuteTicker{0}", minute); } }
使用内置类实现(需要注意UI跨线程问题)
UI线程友好:System.Windows.Forms.Timer、System.Windows.Threading.DispatcherTimer、System.Timers.Timer(要专门配置)
性能友好:System.Threading.Timer