面试官第二问:C#实现一个带值变更通知能力的Dictionary?
姿势在不断的更新迭代, 太卷了。
你管这也叫线程安全?
最近大意了,竟然想将《面试官:实现一个带值变更通知能力的Dictionary》一文中的临界锁只应用到写操作。
内心旁白:读操作又不会修改数据,无论是新值还是旧值,反正能读到。
不过我又快速清醒了,临界锁还真就得这么加。临界锁的目的是保证这一段代码逻辑不会被打断。
假如只应用写锁:
某线程执行到写锁前(刚触发了一次变通通知),这时cpu时间片轮转或抢占, 切换到另外的线程又把这段代码执行了一次(因为字典key-value还没被前线程覆写),这样一次value变更实际执行了两次变更操作,这就悲剧了。
结合之前《你管这叫线程安全?》一文中多线程对于i++
、i--
带来的线程不安全的理解。
你品你细品:
本次线程安全是在宏观代码行执行层面, 上次的i++
是在微观寄存器层面,归根到底让多线程在多核环境下:代码逻辑不能被打断(代码执行节奏可能被打断)。
多线程环境下,程序运行真是危机四伏。
微软官方怎么说?
还没完, 我还从微软官方原子操作[1]找到一段话:
Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list are also atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, are not guaranteed to be atomic. Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement.
直译起来:
① bool char byte sbyte uint int float 和引用类型上的读写是原子操作;
② 由以上类型定义的枚举类型操作也是原子类型;
③ long ulong double decimal和用户定义类型上的读写不保证是原子操作;
④ 除了库文件本身设计了线程安全,一般况下下都不保证读写是原子操作, 这也包括i++
、i--
这段文字是不是刷新了某些童靴的认知(包括在下):
1. 以后使用long num=8888;
时要留个心眼,你也许会读到long类型的部分字节。2.直译第①点说引用类型的读写是原子操作,第③点说用户类型不保证原子操作,但是大部分的用户类型是引用类型,这不互相矛盾了吗?
我向微软官方提出了我的这个疑问,有兴趣可以关注这个github issue[2]。
说说我的看法:
直译第①点中说int,引用类型等读写操作是原子操作:应该想表达的是纯粹的赋值操作, 比如
int a =1; // 赋值:线程安全
Student s = new Student {}; // 引用赋值:线程安全
Student s2= s; // 引用赋值:线程安全
针对引用类型Dictionary的其他操作自然不是线程安全的。
依据这个思路, 第①③点就不矛盾了。
That's All, 本文依旧是#线程安全#、#锁# 这两个老生常谈的概念的延续, 我的知识体系也是在不断迭代更新,不断精炼。
我也没想到越挖越深,真的是太卷了,如有不同的看法,请留言交流。
引用链接
[1]
微软官方原子操作: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables[2]
github issue: https://github.com/dotnet/csharpstandard/issues/372