Java和C#通过检查数组范围和指针取消引用来提供内存安全性。
可以在编程语言中实现哪些机制来防止出现竞争状况和死锁的可能性?
Java和C#通过检查数组范围和指针取消引用来提供内存安全性。
可以在编程语言中实现哪些机制来防止出现竞争状况和死锁的可能性?
Answers:
当您同时具有对象的别名并且至少其中一个别名正在变异时,就会发生竞争。
因此,为防止种族冲突,您需要使其中一个或多个条件不成立。
各种方法处理各个方面。函数式编程强调不变性,从而消除了可变性。锁定/原子消除了同时性。仿射类型删除别名(Rust删除可变别名)。角色模型通常会消除锯齿。
您可以限制可以别名的对象,以便更容易确保避免上述情况。这就是通道和/或消息传递样式的来源。您不能为任意内存加上别名,而只能将通道或队列的末尾安排为无竞争的。通常通过避免同时性,即锁或原子。
这些各种机制的缺点是它们限制了您可以编写的程序。限制越钝,程序越少。因此,没有混叠或可变性的工作,很容易推论,但有很大的局限性。
这就是Rust引起如此轰动的原因。这是一种工程语言(相对于学术语言而言),它支持别名和可变性,但让编译器检查它们不会同时发生。尽管不是理想的方法,但它确实允许比许多以前的版本安全地编写更大种类的程序。
Java和C#通过检查数组范围和指针取消引用来提供内存安全性。
首先考虑C#和Java如何做到这一点很重要。他们通过将C或C ++中未定义的行为转换为已定义的行为来实现:崩溃程序。在正确的C#或Java程序中,绝不能捕获空引用和数组索引异常。首先不应该发生这些错误,因为该程序不应存在该错误。
但这不是我想问的意思!我们可以很容易地编写一个“防死锁”运行时,该运行时定期检查是否有n个线程互相等待,并在发生这种情况时终止程序,但是我认为这不会满足您的要求。
可以在编程语言中实现哪些机制来防止出现竞争状况和死锁的可能性?
我们要面对的下一个问题是,与死锁不同,“竞赛条件”很难检测。请记住,我们在线程安全方面追求的并不是消除种族。无论谁赢得比赛,我们追求的都是使程序正确!竞争条件的问题不是两个线程都以未定义的顺序运行,而且我们不知道谁先完成。竞争条件的问题在于,开发人员忘记了线程完成的某些顺序是可能的,而没有考虑到这种可能性。
因此,您的问题基本上可以归结为“一种编程语言可以确保我的程序正确的方法吗?” 这个问题的答案实际上是不。
到目前为止,我只批评了你的问题。让我在这里尝试切换一下方式,解决您提出问题的精神。语言设计者是否可以做出选择,以减轻我们在多线程中遇到的可怕情况?
情况真是太可怕了!要正确执行多线程代码,特别是在弱内存模型体系结构上,非常非常困难。考虑一下为什么困难是很有启发性的:
因此,语言设计师有一种显而易见的方法可以使事情变得更好。放弃现代处理器的性能优势。使所有程序(甚至是多线程程序)都具有非常强大的内存模型。这会使多线程程序变慢许多倍,这直接与首先拥有多线程程序的原因相反:为了提高性能。
甚至不考虑内存模型,还有其他原因导致多线程难以实现:
最后一点需要进一步解释。所谓“可组合的”是指:
假设我们希望计算给定double的int。我们编写计算的正确实现:
int F(double x) { correct implementation here }
假设我们希望计算一个给定整数的字符串:
string G(int y) { correct implementation here }
现在,如果我们要计算给定双精度值的字符串:
double d = whatever;
string r = G(F(d));
G和F可以构成对更复杂问题的正确解决方案。
但是由于死锁,锁没有此属性。在不创建错误程序的情况下,不能在同一程序中同时使用按L1,L2顺序锁定的正确方法M1和按L2,L1顺序锁定的正确方法M2。锁使得它不能说“每个方法都是正确的,所以整个事情都是正确的”。
那么,作为语言设计师,我们能做什么?
首先,不要去那里。在一个程序中使用多个控制线程是一个坏主意,并且在线程之间共享内存是一个坏主意,因此不要一开始就将其放入语言或运行时中。
显然,这不是一个入门者。
然后让我们把注意力转向一个更基本的问题:为什么我们首先要有多个线程?有两个主要原因,尽管它们有很大的不同,但它们经常合并到同一件事中。之所以将它们混为一谈是因为它们都与管理延迟有关。
馊主意。而是通过协程使用单线程异步。C#做到了这一点。Java,不是很好。但这是当前大量语言设计师帮助解决线程问题的主要方式。await
C#中 的运算符(受F#异步工作流和其他现有技术的启发)正在被集成到越来越多的语言中。
语言设计师可以通过创建与并行性很好地配合使用的语言功能来提供帮助。例如,考虑一下LINQ如何自然地扩展到PLINQ。如果您是一个明智的人,并且将TPL操作限制为高度并行且不共享内存的受CPU约束的操作,那么您可以在这里获得巨大成功。
我们还能做什么?
C#不允许您等待锁,因为这是死锁的秘诀。C#不允许您锁定值类型,因为这样做总是错误的。您锁定框,而不是值。如果您别名为volatile,C#会警告您,因为别名不会强加获取/释放语义。编译器还有很多其他方法可以检测到常见问题并加以预防。
通过允许您将任何引用对象用作监视器,C#和Java犯了一个巨大的设计错误。这鼓励了各种各样的不良做法,这使得更难跟踪死锁,并且更难以静态地防止死锁。并且浪费每个对象头中的字节。监视器应被要求从监视器类派生。
STM是个好主意,我在Haskell从事玩具的实现。与基于锁的解决方案相比,它使您可以从正确的零件中更优雅地构成正确的解决方案。但是,我对这些细节还不够了解,无法解释为什么不能大规模地使用它。你下次见乔·达菲时问他。
对基于过程演算的语言进行了大量研究,但我对这个空间不是很了解。尝试自己阅读一些论文,看看是否有任何见解。
在Microsoft在Roslyn工作之后,我在Coverity工作,我要做的一件事情就是使用Roslyn获得分析仪前端。通过由Microsoft提供准确的词法,句法和语义分析,我们可以集中精力编写发现常见多线程问题的检测器。
我们之所以会出现种族和僵局以及所有这些东西,根本原因是因为我们正在编写说明要做什么的程序,结果证明我们都对编写命令性程序很不满意。计算机会按照您说的去做,而我们会告诉它做错了事。许多现代编程语言越来越多地使用声明式编程:说出所需的结果,然后让编译器找出实现该结果的有效,安全,正确的方法。再次考虑一下LINQ;我们要你说from c in customers select c.FirstName
,这表达了一种意图。让编译器弄清楚如何编写代码。
机器学习算法在某些任务上比手工编码算法要好得多,尽管当然要进行很多权衡,包括正确性,训练时间,不良训练带来的偏见等。但是很可能我们当前“手动”编写的许多任务很快将适用于机器生成的解决方案。如果人类没有编写代码,那么他们就没有编写错误。
抱歉,那儿有点杂乱无章。这是一个巨大而艰巨的主题,在我追踪问题领域的进展的20年里,PL社区尚未达成明确共识。