ChatGPT解决这个技术问题 Extra ChatGPT

术语“线程安全”是什么意思?

这是否意味着两个线程不能同时更改底层数据?或者这是否意味着当多个线程正在执行该代码段时,给定的代码段将以可预测的结果运行?

刚刚在这里看到了一个关于这个问题的有趣讨论:blogs.msdn.com/ericlippert/archive/2009/10/19/…
这是新链接:docs.microsoft.com/en-us/archive/blogs/ericlippert/… 用于 Sebastian 分享的文章

M
Marek Blotny

线程安全代码是即使许多线程同时执行它也能工作的代码。

http://mindprod.com/jgloss/threadsafe.html


确实,在同一个过程中:)
“编写可以稳定运行数周的代码需要极度的偏执狂。”这是我喜欢的一句话:)
呃!这个答案只是重申了这个问题! --- 为什么只在同一个过程中???如果当多个线程从不同进程执行代码时代码失败,那么可以说(“共享内存”可能在磁盘文件中),它不是线程安全的!
请注意,@CharlesBretana 在这里使用了更概念化(和直观?)的“线程”定义,以涵盖不涉及实际线程的潜在多处理场景。 (在 Python 中,有完整的框架可以在没有线程或共享内存/磁盘的情况下执行此操作,而是通过将腌制对象作为消息传递。)
@mg30rg。也许混乱是由于某种原因认为当一个代码块被多个进程执行时,但每个进程只有一个线程,那不知何故仍然是“单线程”场景,而不是多线程场景.这个想法甚至没有错。这只是错误的定义。显然,多个进程通常不会以同步的方式在同一个线程上执行(除非在极少数情况下,进程通过设计相互协调并且操作系统在进程之间共享线程。)
J
Joe McMahon

一个更具信息性的问题是什么使代码不是线程安全的 - 答案是有四个条件必须为真......想象一下下面的代码(它是机器语言翻译)

totalRequests = totalRequests + 1
MOV EAX, [totalRequests]   // load memory for tot Requests into register
INC EAX                    // update register
MOV [totalRequests], EAX   // store updated value back to memory

第一个条件是可以从多个线程访问的内存位置。通常,这些位置是全局/静态变量,或者是可从全局/静态变量访问的堆内存。每个线程都为函数/方法范围的局部变量获取自己的堆栈框架,因此这些局部函数/方法变量 otoh(位于堆栈上)只能从拥有该堆栈的一个线程访问。第二个条件是存在与这些共享内存位置相关联的属性(通常称为不变量),该属性必须为真或有效,程序才能正常运行。在上面的示例中,属性是“totalRequests 必须准确地表示任何线程已执行增量语句的任何部分的总次数”。通常,此不变属性需要在更新发生之前保持为真(在这种情况下,totalRequests 必须保持准确的计数)才能使更新正确。第三个条件是不变量属性在实际更新的某些部分不成立。 (它在处理的某些部分暂时无效或错误)。在这种特殊情况下,从获取 totalRequests 到存储更新值的时间,totalRequests 不满足不变量。竞争发生必须发生的第四个也是最后一个条件(因此代码不是“线程安全的”)是另一个线程必须能够在不变量被破坏时访问共享内存,从而导致不一致或不正确的行为。


这仅涵盖所谓的数据竞争,当然很重要。然而,还有其他方式导致代码无法实现线程安全——例如可能导致死锁的错误锁定。即使是像在 java 线程中的某处调用 System.exit() 这样简单的事情也会使该代码不是线程安全的。
我想在某种程度上这是语义,但我认为可能导致死锁的错误锁定代码不会使代码不安全。首先,除非如上所述的竞争条件是可能的,否则不需要首先锁定代码。然后,如果您以导致死锁的方式编写锁定代码,那不是线程不安全的,它只是糟糕的代码。
但是请注意,在单线程运行时不会发生死锁,因此对于我们大多数人来说,这肯定属于(不是)“线程安全”的直观含义。
好吧,当然,除非您运行多线程,否则不会发生死锁,但这就像说如果您在一台机器上运行就不会发生网络问题。其他问题也可能发生在单线程中,如果程序员编写代码以便在完成更新之前突破关键代码行,并在其他一些子程序中修改变量。
请使用称为“伪代码”的与语言无关的代码行来解释这些概念,因为问题中没有提及汇编语言。
B
Buu

我喜欢 Brian Goetz 的 Java Concurrency in Practice 中的定义,因为它的全面性

“如果一个类在从多个线程访问时行为正确,则该类是线程安全的,无论运行时环境对这些线程执行的调度或交错如何,并且调用代码部分没有额外的同步或其他协调。 "


这个定义是不完整的,不具体的,也绝对不全面。它必须安全运行多少次,就一次?十次?每次? 80%的时间?并且它没有指定是什么使它“不安全”。如果它无法安全运行,但失败是因为除以零错误,这是否使它成为线程“不安全”?
下次再文明一点,也许我们可以讨论一下。这不是 Reddit,我没有心情与粗鲁的人交谈。
您将别人的定义解释为对自己的侮辱的评论很能说明问题。在做出情绪反应之前,您需要阅读并理解实质内容。我的评论没有什么不文明的。我在说明定义的含义。对不起,如果我用来说明这一点的例子让你不舒服。
Y
Yangshun Tay

一种更容易理解的方法是使代码不是线程安全的。有两个主要问题会使线程应用程序具有不需要的行为。

在不锁定的情况下访问共享变量 此变量可以在执行函数时被另一个线程修改。您想通过锁定机制来防止它,以确保您的函数的行为。一般的经验法则是尽可能缩短锁定时间。

由共享变量相互依赖引起的死锁如果你有两个共享变量 A 和 B。在一个函数中,你先锁定 A,然后再锁定 B。在另一个函数中,你开始锁定 B,一段时间后,你锁定 A。这是一个潜在的死锁,其中第一个函数将等待 B 被解锁,而第二个函数将等待 A 被解锁。此问题可能不会在您的开发环境中发生,并且只会不时发生。为避免这种情况,所有锁必须始终保持相同的顺序。


好一个,解释问题应该总是在解释解决方案之前解释。
M
Marcus Downing

正如其他人指出的那样,线程安全意味着如果一段代码同时被多个线程使用,则它可以正常工作。

值得注意的是,这有时会以计算机时间和更复杂的编码为代价,因此并不总是可取的。如果一个类只能在一个线程上安全地使用,那么这样做可能会更好。

例如,Java 有两个几乎等价的类,StringBufferStringBuilder。不同之处在于 StringBuffer 是线程安全的,因此多个线程可以同时使用 StringBuffer 的单个实例。 StringBuilder 不是线程安全的,并且被设计为当 String 仅由一个线程构建时的那些情况(绝大多数)的更高性能替代品。


M
Mnementh

线程安全代码按规定工作,即使不同线程同时输入。这通常意味着,应该不间断地运行的内部数据结构或操作同时受到不同修改的保护。


D
Daniel Russell

至少在 C++ 中,我认为线程安全有点用词不当,因为它留下了很多名字。为了线程安全,代码通常必须主动处理它。这通常不是一种被动的品质。

对于线程安全的类,它必须具有增加开销的“额外”功能。这些特性是类实现的一部分,一般来说,对接口是隐藏的。也就是说,不同的线程可以访问任何类的成员,而不必担心与不同线程的并发访问发生冲突,并且可以以非常懒惰的方式这样做,使用一些普通的老式常规人类编码风格,而不必做所有已经卷入被调用代码内部的疯狂同步工作。

这就是为什么有些人更喜欢使用内部同步这个术语。

术语集

我遇到的这些想法主要有三组术语。第一个也是历史上更受欢迎(但最差)的是:

线程安全不是线程安全的

第二个(更好的)是:

线程证明线程兼容线程敌对

第三个是(甚至更好)一个是:

内部同步 外部同步 不可同步

类比

线程安全~线程证明~内部同步

内部同步(也称为线程安全或线程证明)系统的一个示例是餐厅,主人在门口迎接您,并禁止您自己排队。主人是餐厅处理多个顾客的机制的一部分,并且可以使用一些相当棘手的技巧来优化等待顾客的座位,比如考虑他们的聚会规模,或者他们看起来有多少时间,甚至通过电话进行预订。餐厅是内部同步的,因为当您与之互动时,所有这些都包含在“幕后”。你,客户,不要做任何事情。房东会为你做这一切。

不是线程安全的(但很好)~线程兼容~外部同步~自由线程

假设你去银行。有一条线,即银行柜员的竞争。因为你不是野蛮人,所以你认识到在争夺资源时最好的办法就是像文明人一样排队。没有人在技术上让你这样做。我们希望你有必要的社交编程来自己做这件事。从这个意义上说,银行大厅是外部同步的。

我们应该说它是线程不安全的吗?如果您使用线程安全、线程不安全的双极术语集,这就是含义。这不是一组很好的术语。更好的术语是外部同步,银行大厅对被多个客户访问并不怀有敌意,但它也不做同步他们的工作。客户自己这样做。

这也称为“free threaded”,其中“free”与“free from lice”一样——或者在这种情况下是锁。好吧,更准确地说,是同步原语。这并不意味着代码可以在没有这些原语的情况下在多个线程上运行。这只是意味着它没有随它们一起安装,它取决于你,代码的用户,你自己安装它们,但你认为合适。安装您自己的同步原语可能很困难并且需要仔细考虑代码,但也可以通过允许您自定义程序在当今超线程 CPU 上的执行方式来实现最快的程序。

不是线程安全的(而且不好)~线程敌对~不可同步

一个线程敌对系统的日常类比示例是一个混蛋,一辆跑车拒绝使用他们的闪光灯并随意改变车道。他们的驾驶风格是线程敌对或不可同步的,因为您无法与他们协调,这可能导致争用同一车道,没有解决方案,因此当两辆车试图占据同一空间时发生事故,没有任何协议防止这种情况。这种模式也可以更广泛地被认为是反社会的,尽管它不是特定于线程,而是更普遍地适用于编程的许多领域。

为什么线程安全/非线程安全是一个不好的术语集

第一个也是最古老的术语集未能在线程敌意和线程兼容性之间做出更精细的区分。线程兼容性比所谓的线程安全更被动,但这并不意味着被调用的代码对于并发线程使用是不安全的。这只是意味着它对允许这样做的同步是被动的,将其推迟到调用代码中,而不是将其作为其内部实现的一部分提供。在大多数情况下,线程兼容是默认编写代码的方式,但遗憾的是,这也经常被错误地认为是线程不安全的,好像它本质上是反安全的,这对程序员来说是一个主要的困惑点。

注意:许多软件手册实际上使用术语“线程安全”来指代“线程兼容”,这给已经一团糟的东西增添了更多的混乱!出于这个原因,我不惜一切代价避免使用术语“线程安全”和“线程不安全”,因为一些消息来源会称其为“线程安全”,而其他人会称其为“线程不安全”,因为他们不能同意关于您是否必须满足一些额外的安全标准(预先安装的同步原语),或者只是不敌对才能被认为是“安全的”。因此,请避免使用这些术语并改用更智能的术语,以避免与其他工程师发生危险的错误沟通。

提醒我们的目标

本质上,我们的目标是颠覆混乱。

我们通过创建我们可以依赖的半确定性系统来做到这一点。确定性是昂贵的,主要是由于失去并行性、流水线和重新排序的机会成本。我们试图尽量减少我们需要的确定性以保持低成本,同时避免做出会进一步削弱我们能够承受的微小确定性的决策。因此,半前缀。我们只希望代码状态的某些小部分是确定性的,而底层的计算机制不必完全如此。线程的同步是关于增加多线程系统中的顺序并减少混乱,因为拥有多个线程自然会导致大量的非确定性,必须以某种方式抑制这种不确定性。

总而言之,一些代码体可以在三个主要程度的努力上投入“杂耍刀”——即在多线程的上下文中正确工作。

最高程度(线程证明等)意味着系统以可预测的方式运行,即使您从多个线程中草率地调用它。它自己完成了实现这一目标所需的工作,因此您不必这样做。它为您(编写调用代码的程序员)提供了这个漂亮的界面,这样您就可以假装生活在一个没有同步原语的世界中。因为它已经在内部包含了它们。它也很昂贵,速度很慢,而且由于它正在执行同步而完成任务所需的时间也有些不可预测,这必须始终大于您特定程序所需的数量,因为它不知道您的代码是什么会做。非常适合使用各种脚本语言进行科学或其他事情的临时编码人员,但他们自己并没有编写高效的接近金属的代码。他们不需要杂耍刀。

第二级(线程兼容等)意味着系统表现得足够好,调用代码可以及时可靠地检测到不可预测性,以便在运行时使用自己安装的同步原语正确处理它。 DIY同步。 BYOSP = 自带同步原语。至少你知道你调用的代码会很好地适应它们。这适用于更接近金属的专业程序员。

第三级(线程敌对等)意味着系统表现得不够好,无法与其他任何人一起玩,并且只能单线程运行而不会引起混乱。本质上,这是经典的 90 年代早期和更早的代码。它的编程缺乏对如何从多个线程调用或使用它的高度意识,以至于即使您尝试自己添加这些同步原语,它也无法正常工作,因为它做出了老式的假设,即这些日子似乎反社会和不专业。

但是,某些代码只有在称为单线程时才真正有意义,因此仍会故意以这种方式调用。对于已经具有高效流水线和内存访问序列的软件尤其如此,并且没有受益于多线程的主要目的:隐藏内存访问延迟。访问非高速缓存内存比大多数其他指令慢得多。因此,每当应用程序等待一些内存访问时,它应该同时切换到另一个任务线程以保持处理器工作。当然,这些天来,这可能意味着切换到另一个协程/光纤/等。在同一个线程中,如果可用,因为它们比线程上下文切换更有效。但是一旦暂时用尽了这些,就该切换在我们的核心上执行的线程了。

但是有时,您的所有内存访问都被很好地打包和排序,而您最不想做的就是切换到另一个线程,因为您已经将代码流水线化以尽可能有效地处理这个问题。然后线程受伤无济于事。这是一个例子,但还有其他例子。

总的来说,我认为在编写要调用的代码时尽可能使用线程兼容是有意义的,特别是如果没有真正的理由不这样做,并且它只需要您在编写代码时的意识。


t
tvanfosson

不要将线程安全与确定性混淆。线程安全代码也可以是非确定性的。鉴于调试线程代码问题的难度,这可能是正常情况。 :-)

线程安全只是确保当一个线程正在修改或读取共享数据时,没有其他线程可以以更改数据的方式访问它。如果您的代码依赖于特定的执行顺序以确保正确性,那么您需要除了线程安全所需的其他同步机制来确保这一点。


B
Bill the Lizard

是和不是。

线程安全不仅仅是确保您的共享数据一次只能由一个线程访问。您必须确保按顺序访问共享数据,同时避免使用 race conditionsdeadlockslivelocksresource starvation

多个线程运行时的不可预测结果不是线程安全代码的必要条件,但它通常是副产品。例如,您可以设置一个 producer-consumer 方案,其中包含一个共享队列、一个生产者线程和几个消费者线程,并且数据流可能是完全可预测的。如果您开始介绍更多的消费者,您会看到更多随机的结果。


C
Community

从本质上讲,在多线程环境中,很多事情都可能出错(指令重新排序、部分构造的对象、由于 CPU 级别的缓存,相同的变量在不同的线程中具有不同的值等)。

我喜欢 Java Concurrency in Practice 给出的定义:

如果一个[代码部分]在从多个线程访问时行为正确,则它是线程安全的,无论运行时环境对这些线程执行的调度或交错,并且没有额外的同步或其他协调调用代码。

正确的意思是程序的行为符合其规范。

人为的例子

想象一下,您实现了一个计数器。您可以说它在以下情况下表现正确:

counter.next() 永远不会返回之前已经返回的值(为简单起见,我们假设没有溢出等)

从 0 到当前值的所有值都已在某个阶段返回(没有跳过任何值)

线程安全计数器将根据这些规则运行,无论有多少线程同时访问它(这通常不是简单实现的情况)。

注意:cross-post on Programmers


A
Alireza Fattahi

简单地说 - 如果许多线程同时执行此代码,代码将运行良好。


F
Felix K.

让我们通过例子来回答这个问题:

class NonThreadSafe {

    private int count = 0;

    public boolean countTo10() {
        count = count + 1;
        return (count == 10);
    }

countTo10 方法将计数器加一,然后在计数达到 10 时返回 true。它应该只返回一次 true。

只要只有一个线程在运行代码,这将起作用。如果两个线程同时运行代码,可能会出现各种问题。

例如,如果 count 从 9 开始,一个线程可以将 1 加到 count (使 10),但随后第二个线程可以进入该方法并在第一个线程有机会执行与 10 的比较之前再次加 1(使 11) . 然后两个线程都做比较,发现count都是11,都没有返回true。

所以这段代码不是线程安全的。

本质上,所有的多线程问题都是由这类问题的某种变体引起的。

解决方案是确保加法和比较不能分开(例如,通过某种同步代码将两条语句包围起来)或设计一种不需要两次操作的解决方案。这样的代码将是线程安全的。


C
Community

我想在其他好的答案之上添加更多信息。

线程安全意味着多个线程可以在同一个对象中写入/读取数据而不会出现内存不一致错误。在高度多线程的程序中,线程安全的程序不会对共享数据造成副作用。

查看此 SE 问题以了解更多详细信息:

What does threadsafe mean?

线程安全程序保证内存一致性。

来自有关高级并发 API 的 Oracle 文档 page

内存一致性属性:

Java™ 语言规范的第 17 章定义了内存操作(例如共享变量的读取和写入)的发生前关系。只有当写操作发生在读操作之前,一个线程写的结果才能保证对另一个线程的读可见。

synchronizedvolatile 构造以及 Thread.start()Thread.join() 方法可以形成 happens-before 关系。

java.util.concurrent 及其子包中所有类的方法将这些保证扩展到更高级别的同步。 特别是:

在将对象放入任何并发集合之前线程中的操作发生在另一个线程中从集合中访问或删除该元素之后的操作。在将 Runnable 提交给 Executor 之前,线程中的操作发生在其执行开始之前。对于提交给 ExecutorService 的 Callables 也是如此。在另一个线程中通过 Future.get() 检索结果之后,由 Future 表示的异步计算所采取的操作发生在操作之前。 “释放”同步器方法(例如 Lock.unlock、Semaphore.release 和 CountDownLatch.countDown)之前的操作发生在成功“获取”方法(例如 Lock.lock、Semaphore.acquire、Condition.await 和 CountDownLatch)之后的操作.await 在另一个线程中的同一个同步器对象上。对于通过 Exchanger 成功交换对象的每对线程,每个线程中 exchange() 之前的操作发生在另一个线程中相应 exchange() 之后的操作之前。调用 CyclicBarrier.await 和 Phaser.awaitAdvance(及其变体)之前的操作发生在屏障操作执行的操作之前,以及屏障操作执行的操作发生在从其他中的相应等待成功返回之后的操作线程。


V
VonC

要完成其他答案:

仅当您的方法中的代码执行以下两种操作之一时,同步才令人担忧:

与一些不是线程安全的外部资源一起使用。读取或更改持久对象或类字段

这意味着在您的方法中定义的变量始终是线程安全的。对方法的每次调用都有自己的这些变量版本。如果该方法被另一个线程调用,或者被同一个线程调用,或者即使该方法调用自身(递归),这些变量的值也不会共享。

线程调度不保证是round-robin。一个任务可能以相同优先级的线程为代价完全占用 CPU。你可以使用 Thread.yield() 有良心。您可以使用(在 java 中)Thread.setPriority(Thread.NORM_PRIORITY-1) 来降低线程的优先级

另外要注意:

迭代这些“线程安全”结构的应用程序的大量运行时成本(其他人已经提到过)。

Thread.sleep(5000) 应该休眠 5 秒。但是,如果有人更改系统时间,您可能会睡很长时间或根本没有时间。操作系统以绝对形式记录唤醒时间,而不是相对形式。


S
Steve Knight

是的,是的。这意味着数据不会被多个线程同时修改。但是,您的程序可能会按预期工作,并且看起来是线程安全的,即使它根本不是。

请注意,结果的不可预测性是“竞争条件”的结果,这可能导致数据以不同于预期的顺序被修改。


s
supercat

与其将代码或类视为线程安全与否,我认为将动作视为线程安全更有帮助。如果两个动作在从任意线程上下文运行时按照指定的方式运行,则它们是线程安全的。在许多情况下,类将支持线程安全方式的某些操作组合,而其他则不支持。

例如,许多集合(如数组列表和哈希集)将保证,如果它们最初仅由一个线程访问,并且在引用对任何其他线程可见后它们永远不会被修改,则它们可以通过任何组合以任意方式读取线程无干扰。

更有趣的是,一些哈希集集合,例如 .NET 中的原始非泛型集合,可以保证只要没有项目被删除,并且只要一个线程向它们写入,任何试图读取集合的行为就像访问一个集合,其中更新可能会延迟并以任意顺序发生,但否则会正常运行。如果线程#1 添加X,然后添加Y,线程#2 查找并看到Y,然后是X,线程#2 可能会看到Y 存在但X 不存在;这种行为是否是“线程安全的”取决于线程#2 是否准备好处理这种可能性。

最后一点,一些类——尤其是阻塞通信库——可能有一个“close”或“Dispose”方法,相对于所有其他方法是线程安全的,但没有其他方法是线程安全的彼此。如果线程执行阻塞读取请求并且程序的用户单击“取消”,则尝试执行读取的线程将无法发出关闭请求。然而,关闭/释放请求可能会异步设置一个标志,这将导致读取请求尽快被取消。一旦在任何线程上执行关闭,对象将变得无用,并且所有对未来操作的尝试都将立即失败,但是能够异步终止任何尝试的 I/O 操作比要求关闭请求与读取同步更好(因为如果读取永远阻塞,同步请求也会被阻塞)。


s
shabby

用最简单的话来说:P 如果在一个代码块上执行多个线程是安全的,那么它就是线程安全的*

*适用条件

其他答案(例如1)提到了条件。如果您在其上执行一个线程或多个线程等,结果应该是相同的。