类似于Java和C#提供内存安全性的方式,如何通过编程语言提供线程安全性?


10

Java和C#通过检查数组范围和指针取消引用来提供内存安全性。

可以在编程语言中实现哪些机制来防止出现竞争状况和死锁的可能性?


3
:您可能会感兴趣的是什么呢锈霍元甲并发铁锈
文森特Savard

2
使一切保持不变,或使一切与安全通道保持异步。您可能也对GoErlang感兴趣。
Theraot

@Theraot“使所有内容与安全通道保持异步”-希望您能对此进行详细说明。
mrpyo

2
@mrpyo,您不会公开进程或线程,每个调用都是一个承诺,所有操作并发运行(运行时调度它们的执行,并根据需要在后台创建/缓冲系统线程),并且保护状态的逻辑在机制中它可以传递信息...运行时可以通过调度自动进行序列化,并且将存在一个带有线程安全解决方案的标准库,用于解决更多细微的行为,尤其是需要生产者/消费者和聚合的情况。
Theraot

2
顺便说一句,还有另一种可能的方法:事务性内存
Theraot

Answers:


14

当您同时具有对象的别名并且至少其中一个别名正在变异时,就会发生竞争。

因此,为防止种族冲突,您需要使其中一个或多个条件不成立。

各种方法处理各个方面。函数式编程强调不变性,从而消除了可变性。锁定/原子消除了同时性。仿射类型删除别名(Rust删除可变别名)。角色模型通常会消除锯齿。

您可以限制可以别名的对象,以便更容易确保避免上述情况。这就是通道和/或消息传递样式的来源。您不能为任意内存加上别名,而只能将通道或队列的末尾安排为无竞争的。通常通过避免同时性,即锁或原子。

这些各种机制的缺点是它们限制了您可以编写的程序。限制越钝,程序越少。因此,没有混叠或可变性的工作,很容易推论,但有很大的局限性。

这就是Rust引起如此轰动的原因。这是一种工程语言(相对于学术语言而言),它支持别名和可变性,但让编译器检查它们不会同时发生。尽管不是理想的方法,但它确实允许比许多以前的版本安全地编写更大种类的程序。


11

Java和C#通过检查数组范围和指针取消引用来提供内存安全性。

首先考虑C#和Java如何做到这一点很重要。他们通过将C或C ++中未定义的行为转换为已定义的行为来实现:崩溃程序。在正确的C#或Java程序中,绝不能捕获空引用和数组索引异常。首先不应该发生这些错误,因为该程序不应存在该错误。

但这不是我想问的意思!我们可以很容易地编写一个“防死锁”运行时,该运行时定期检查是否有n个线程互相等待,并在发生这种情况时终止程序,但是我认为这不会满足您的要求。

可以在编程语言中实现哪些机制来防止出现竞争状况和死锁的可能性?

我们要面对的下一个问题是,与死锁不同,“竞赛条件”很难检测。请记住,我们在线程安全方面追求的并不是消除种族无论谁赢得比赛,我们追求的都是使程序正确!竞争条件的问题不是两个线程都以未定义的顺序运行,而且我们不知道谁先完成。竞争条件的问题在于,开发人员忘记了线程完成的某些顺序是可能的,而没有考虑到这种可能性。

因此,您的问题基本上可以归结为“一种编程语言可以确保我的程序正确的方法吗?” 这个问题的答案实际上是不。

到目前为止,我只批评了你的问题。让我在这里尝试切换一下方式,解决您提出问题的精神。语言设计者是否可以做出选择,以减轻我们在多线程中遇到的可怕情况?

情况真是太可怕了!要正确执行多线程代码,特别是在弱内存模型体系结构上,非常非常困难。考虑一下为什么困难是很有启发性的:

  • 一个进程中的多个控制线程很难推理。一线程够硬!
  • 在多线程世界中,抽象变得极易泄漏。在单线程世界中,即使程序并非按顺序运行,我们也可以保证程序的行为就像按顺序运行。在多线程世界中,情况已不再如此。在单个线程上不可见的优化变得可见,现在开发人员需要了解那些可能的优化。
  • 但情况变得更糟。C#规范指出,不需要实现具有所有线程都可以同意的一致的读写顺序。根本就没有“种族”并且有明显的赢家的想法实际上是不正确的!考虑一种情况,其中在许多线程上对某些变量进行两次写入和两次读取。在一个明智的世界中,我们可能会认为“好吧,我们不知道谁会赢得比赛,但至少会有一场比赛,有人会赢”。我们不在那个明智的世界中。C#允许多个线程不同意读写的顺序。不一定每个人都在观察一个一致的世界。

因此,语言设计师有一种显而易见的方法可以使事情变得更好。放弃现代处理器的性能优势。使所有程序(甚至是多线程程序)都具有非常强大的内存模型。这会使多线程程序变慢许多倍,这直接与首先拥有多线程程序的原因相反:为了提高性能。

甚至不考虑内存模型,还有其他原因导致多线程难以实现:

  • 防止死锁需要整个程序的分析;您需要知道可以取出锁的全局顺序,并在整个程序中强制执行该顺序,即使该程序由不同组织在不同时间编写的组件组成。
  • 我们为您提供驯服多线程的主要工具是锁,但是不能组成锁

最后一点需要进一步解释。所谓“可组合的”是指:

假设我们希望计算给定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。锁使得它不能说“每个方法都是正确的,所以整个事情都是正确的”。

那么,作为语言设计师,我们能做什么?

首先,不要去那里。在一个程序中使用多个控制线程是一个坏主意,并且在线程之间共享内存是一个坏主意,因此不要一开始就将其放入语言或运行时中。

显然,这不是一个入门者。

然后让我们把注意力转向一个更基本的问题:为什么我们首先要有多个线程?有两个主要原因,尽管它们有很大的不同,但它们经常合并到同一件事中。之所以将它们混为一谈是因为它们都与管理延迟有关。

  • 错误地创建线程来管理IO延迟。需要编写一个大文件,访问一个远程数据库,无论如何,创建一个工作线程而不是锁定UI线程。

馊主意。而是通过协程使用单线程异步。C#做到了这一点。Java,不是很好。但这是当前大量语言设计师帮助解决线程问题的主要方式。awaitC#中 的运算符(受F#异步工作流和其他现有技术的启发)正在被集成到越来越多的语言中。

  • 我们适当地创建线程,以使空闲的CPU占用大量计算量。基本上,我们将线程用作轻量级进程。

语言设计师可以通过创建与并行性很好地配合使用的语言功能来提供帮助。例如,考虑一下LINQ如何自然地扩展到PLINQ。如果您是一个明智的人,并且将TPL操作限制为高度并行且不共享内存的受CPU约束的操作,那么您可以在这里获得巨大成功。

我们还能做什么?

  • 使编译器检测最棘手的错误,并将其转变为警告或错误。

C#不允许您等待锁,因为这是死锁的秘诀。C#不允许您锁定值类型,因为这样做总是错误的。您锁定框,而不是值。如果您别名为volatile,C#会警告您,因为别名不会强加获取/释放语义。编译器还有很多其他方法可以检测到常见问题并加以预防。

  • 设计“质量坑”功能,最自然的方法也是最正确的方法。

通过允许您将任何引用对象用作监视器,C#和Java犯了一个巨大的设计错误。这鼓励了各种各样的不良做法,这使得更难跟踪死​​锁,并且更难以静态地防止死锁。并且浪费每个对象头中的字节。监视器应被要求从监视器类派生。

  • 微软大量的时间和精力试图将软件事务性存储器添加到类似C#的语言中,但是他们从来没有获得足够好的性能来将其合并到主要语言中。

STM是个好主意,我在Haskell从事玩具的实现。与基于锁的解决方案相比,它使您可以从正确的零件中更优雅地构成正确的解决方案。但是,我对这些细节还不够了解,无法解释为什么不能大规模地使用它。你下次见乔·达菲时问他。

  • 另一个答案已经提到了不变性。如果您将不变性与有效的协程结合在一起,则可以将角色模型等功能直接构建到您的语言中;例如,以Erlang为例。

对基于过程演算的语言进行了大量研究,但我对这个空间不是很了解。尝试自己阅读一些论文,看看是否有任何见解。

  • 方便第三方编写出色的分析器

在Microsoft在Roslyn工作之后,我在Coverity工作,我要做的一件事情就是使用Roslyn获得分析仪前端。通过由Microsoft提供准确的词法,句法和语义分析,我们可以集中精力编写发现常见多线程问题的检测器。

  • 提高抽象水平

我们之所以会出现种族和僵局以及所有这些东西,根本原因是因为我们正在编写说明要做什么的程序,结果证明我们都对编写命令性程序很不满意。计算机会按照您说的去做,而我们会告诉它做错了事。许多现代编程语言越来越多地使用声明式编程:说出所需的结果,然后让编译器找出实现该结果的有效,安全,正确的方法。再次考虑一下LINQ;我们要你说from c in customers select c.FirstName,这表达了一种意图。让编译器弄清楚如何编写代码。

  • 使用计算机解决计算机问题。

机器学习算法在某些任务上比手工编码算法要好得多,尽管当然要进行很多权衡,包括正确性,训练时间,不良训练带来的偏见等。但是很可能我们当前“手动”编写的许多任务很快将适用于机器生成的解决方案。如果人类没有编写代码,那么他们就没有编写错误。

抱歉,那儿有点杂乱无章。这是一个巨大而艰巨的主题,在我追踪问题领域的进展的20年里,PL社区尚未达成明确共识。


“因此,您的问题基本上可以归结为“一种编程语言可以确保我的程序正确吗?”,而实际上,该问题的答案是“否”。-实际上,这很有可能-这被称为形式验证,虽然不方便,但我敢肯定,它是在关键软件上例行完成的,因此我认为这是不切实际的。但是您作为语言设计师可能知道这一点……
mrpyo

6
@mrpyo:我很清楚。有很多问题。首先:我曾经参加一个正式的验证会议,MSFT研究团队提出了令人振奋的新结果:他们能够扩展其技术来验证长度不超过20行的多线程程序,并使验证程序在不到一周的时间内运行。这是一个有趣的演示,但对我没有用。我有一个要分析的2000万行程序。
埃里克·利珀特

@mrpyo:第二,正如我提到的,锁的一个大问题是由线程安全方法组成的程序不一定是线程安全程序。正式验证单个方法并不一定会有所帮助,并且对于非平凡程序而言,整个程序分析将非常困难。
埃里克·利珀特

6
@mrpyo:第三,形式分析的主要问题是,从根本上说我们在做什么?我们提出了先决条件和后置条件规范,然后验证程序是否符合该规范。大; 从理论上讲这是完全可行的。规范用什么语言写?如果有一个明确的,可核查的规范语言,然后让我们只写我们在所有的程序是语言和编译那个。我们为什么不这样做呢?因为事实证明,也很难用规范语言编写正确的程序!
埃里克·利珀特

2
可以使用前提条件/后置条件来分析应用程序的正确性(例如,使用编码合同)。但是,这种分析仅在条件可组合的条件下才可行,而锁不是。我还要指出,以允许进行分析的方式编写程序需要认真的纪律。例如,未能严格遵守Liskov替代原理的应用程序倾向于抵制分析。
Brian
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.