不可变数据结构怎么可能不是线程安全的?
在一篇名为“线程安全”的东西是什么?, 埃里克·利珀特 说:
不可变数据结构的线程安全就是确保所有操作中数据的使用在逻辑上是一致的,代价是您正在查看可能已过时的不可变快照。
我认为不可变数据结构的全部意义在于它们不会改变,因此不会过时,因此它们本质上是线程安全的
利珀特在这里是什么意思?
回答
利珀特在这里是什么意思?
我同意我写那个特定部分的方式并不像它想象的那么清楚。
早在 2009 年,我们就在为 Roslyn 设计数据结构——“C# 和 VB 编译器即服务”——因此正在考虑如何在 IDE 中进行分析,一个代码几乎永远不会正确的世界——如果代码是正确的,你为什么要编辑它?- 以及在您键入时每秒更改几次的位置。
我认为不可变数据结构的全部意义在于它们不会改变,因此不会过时,因此它们本质上是线程安全的。
事实上,它们不会改变,这使得它们可能已经过时。考虑 IDE 中的一个常见场景:
using System;
class P
{
static void Main()
{
Console.E
}
}
我们有不可变的数据结构代表你输入“E”之前的世界,我们有一个不可变的数据结构代表你刚刚所做的编辑——敲打字母E——现在是一大堆东西发生。
词法分析器知道先前的词法状态是不可变的并且在“E”重新词法围绕 E的世界之前匹配世界,而不是重新词法整个令牌流。类似地,解析器计算出用于此编辑的新的(格式错误的!)解析树是什么。这将创建一个新的不可变解析树,它是旧的不可变解析树的编辑,然后真正的乐趣开始了。语义分析器试图找出什么Console意思,然后你可能意味着什么,E以便它可以做一个以System.Console开头的成员为中心的智能感知下拉菜单E,如果有的话。(而且我们还开始了错误报告工作流程,因为现在程序中存在许多语义和句法错误。)
现在,如果当我们在后台线程上处理所有这些时,您点击“退格”然后点击“W”会发生什么?
所有仍在进行中的分析都是正确的,但它对Console.E和 不正确Console.W。分析已经过时了。它属于一个不再相关的世界,我们必须重新开始分析退格和 W。
简而言之,在另一个线程上分析不可变数据结构是完全安全的,但在 UI 线程上可能会继续发生使该工作无效的事情;这是您为将不可变数据转移到工作线程的工作而支付的价格之一。
请记住,这些失效可能发生得非常快;我们为 re-lex、re-parse 和 IntelliSense 更新预算了 30 毫秒,因为快速打字员每秒可以输入超过 10 次击键;拥有一个重用过去词法和解析的不可变状态的词法分析器和解析器是该策略的关键部分,但是您必须计划一个失效,以同样快地丢弃您当前的分析。
顺便说一下,我们需要发明来有效地跟踪这些失效的机制本身就非常有趣,并导致对基于取消的工作流的一些见解——但这是另一天的主题。
- Wow. You were clearly dealing with a much nastier problem than I am. I'm just trying to deal with a very rare bug where one thread modifies a list that's in the middle of being enumerated in another thread. It's rare only because the time window when it can happen is very small, but it does cause considerable damage. The fix is just to make a ConcurrentList<T>, which uses locking internally, and when you call GetEnumerator, it clones the list and returns the enumerator for the copy, thus an "immutable snapshot". Your article really helped me understand how to think about the whole situation.