什么是“线程安全”代码?

时间:2008-11-04 12:14:05

标签: multithreading language-agnostic programming-languages concurrency

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

18 个答案:

答案 0 :(得分:242)

来自维基百科:

线程安全是一种适用于多线程程序环境的计算机编程概念。如果一段代码在多个线程同时执行期间正常运行,则它是线程安全的。特别是,它必须满足多个线程访问相同共享数据的需要,并且在任何给定时间只需要一个线程访问共享数据。

有几种方法可以实现线程安全:

Re-entrancy:

以这样一种方式编写代码,即它可以由一个任务部分执行,由另一个任务重新输入,然后从原始任务恢复。这需要将状态信息保存在每个任务的本地变量中,通常在其堆栈中,而不是静态或全局变量中。

相互排斥:

使用确保只有一个线程可以随时读取或写入共享数据的机制来序列化对共享数据的访问。如果一段代码访问多个共享数据,则需要非常小心 - 问题包括竞争条件,死锁,活锁,饥饿以及许多操作系统教科书中列举的各种其他问题。

线程本地存储:

变量已本地化,因此每个线程都有自己的私有副本。这些变量在子例程和其他代码边界中保留其值,并且是线程安全的,因为它们是每个线程的本地,即使访问它们的代码可能是可重入的。

原子操作:

使用原子操作访问共享数据,这些操作不能被其他线程中断。这通常需要使用特殊的机器语言指令,这些指令可能在运行时库中可用。由于操作是原子操作,因此无论其他线程访问它,共享数据始终保持有效状态。原子操作构成了许多线程锁定机制的基础。

了解详情:

http://en.wikipedia.org/wiki/Thread_safety


答案 1 :(得分:73)

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

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

答案 2 :(得分:47)

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

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

答案 3 :(得分:32)

我喜欢Brian Goetz的Java Concurrency in Practice的全面性定义

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

答案 4 :(得分:25)

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

值得注意的是,这有时会带来成本,计算机时间和更复杂的编码,因此并不总是令人满意。如果一个类只能在一个线程上安全使用,那么这样做可能会更好。

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

答案 5 :(得分:22)

线程安全代码按指定的方式工作,即使由不同的线程同时输入也是如此。这通常意味着,应该不间断运行的内部数据结构或操作可以同时防止不同的修改。

答案 6 :(得分:20)

理解它的一种更简单的方法是使代码不是线程安全的。有两个主要问题会导致线程应用程序出现不需要的行为。

  • 无需锁定即可访问共享变量 执行该函数时,该变量可由另一个线程修改。您希望使用锁定机制来阻止它,以确保您的函数的行为。一般的经验法则是尽可能在最短的时间内保持锁定。

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

答案 7 :(得分:9)

是和否。

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

多个线程运行时出现不可预测的结果是线程安全代码的必需条件,但它通常是副产品。例如,您可以使用共享队列,一个生产者线程和少数消费者线程设置producer-consumer方案,并且数据流可能是完全可预测的。如果你开始引入更多的消费者,你会看到更多随机的结果。

答案 8 :(得分:8)

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

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

  

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

正确意味着程序的行为符合其规范。

已提交的示例

想象一下,你实现了一个计数器。如果出现以下情况,您可以说它的行为正确:

  • counter.next()永远不会返回之前已经返回的值(为简单起见,我们假设没有溢出等)
  • 在某个阶段(不跳过任何值)已返回从0到当前值的所有值

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

注意:cross-post on Programmers

答案 9 :(得分:7)

简单 - 如果许多线程同时执行此代码,代码将运行正常。

答案 10 :(得分:5)

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

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

有关详细信息,请查看此SE问题:

What does threadsafe mean?

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

来自高级并发API的oracle文档page

内存一致性属性:

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

synchronizedvolatile结构以及Thread.start()Thread.join()方法可以形成发生在之前的关系。

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

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

答案 11 :(得分:4)

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

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

答案 12 :(得分:3)

完成其他答案:

当方法中的代码执行以下两项操作之一时,同步只是一种担心:

  1. 使用一些非线程安全的外部资源。
  2. 读取或更改持久对象或类字段
  3. 这意味着在您的方法中定义的变量始终是线程安全的。对方法的每次调用都有自己的这些变量版本。如果该方法由另一个线程或同一个线程调用,或者即使该方法调用自身(递归),也不会共享这些变量的值。

    线程安排不能保证 round-robin 。任务可能会以相同优先级的线程为代价完全占用CPU。你可以使用Thread.yield()来保持良知。您可以使用(在java中)Thread.setPriority(Thread.NORM_PRIORITY-1)来降低线程的优先级

    另外要注意:

    • 迭代这些“线程安全”结构的应用程序的大运行时成本(已被其他人提及)。
    • Thread.sleep(5000)应该睡5秒钟。但是,如果有人改变系统时间,你可能会睡很长时间或根本没有时间。操作系统以绝对形式记录唤醒时间,而不是相对的。

答案 13 :(得分:1)

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

请注意,结果的不可预测性是“竞争条件”的结果,可能导致数据按照预期的顺序进行修改。

答案 14 :(得分:1)

至少在C ++中,我认为 thread-safe 有点用词不当,因为它与名称大相径庭。为了确保线程安全,代码通常必须对此具有 proactive 。通常这不是一种被动的素质。

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

这就是为什么有些人更喜欢使用术语“内部同步”的原因。

术语集

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

  1. 线程安全
  2. 不是线程安全

第二个(更好)是:

  1. 线程证明
  2. 线程兼容
  3. 线程敌对

第三个是:

  1. 内部同步
  2. 外部同步
  3. 不可同步

类比

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

内部同步(又名线程安全线程证明)系统的示例是一家餐馆,主人在这里接待您门,并禁止您排队。主持人是餐厅与多个顾客打交道的机制的一部分,可以使用一些相当棘手的技巧来优化等待顾客的座位,例如考虑到宴会的规模或他们看起来有多少时间,甚至可以通过电话进行预订。该餐厅是内部同步的,因为所有这些都是与餐厅互动的界面的一部分。

不是线程安全的(但很好)〜线程兼容外部同步自由线程 < / p>

假设您去银行。有一条线,即银行出纳员的争用。因为您不是野蛮人,所以您认识到在争用资源时最好的办法是像文明人一样排队。从技术上讲,没人能做到这一点。我们希望您有必要的社交程序来自己做。从这个意义上说,银行大厅是外部同步的。我们应该说它是线程不安全的吗?如果您使用 thread-safe thread-unsafe 双极性术语集,这就是含义。这不是一个很好的术语集。更好的术语是外部同步,银行大厅不欢迎多个客户访问,但也不做同步他们的工作。客户自己做。

这也称为“自由线程”,其中“自由”与“没有虱子”中的情况相同-在这种情况下为锁。好吧,更准确地说,是同步原语。这并不意味着代码可以在没有这些原语的情况下在多个线程上运行。这只是意味着它没有预装它们,而是由代码用户决定是否自行安装它们,这取决于代码的用户。安装自己的同步原语可能很困难,并且需要认真考虑代码,但是通过允许您自定义程序在当今超线程CPU上的执行方式,也可能导致最快的程序。

不是线程安全的(不好)〜线程敌对不可同步

线程敌对系统的一个示例是,当您与欺骗您的人发生关系时,因为即使您被告知您拥有对他们的独占访问权,也不是真的和信任被打破。涉及“数据”的损坏。解决几乎是不可能的,因为系统变得不确定,导致人际交流的含义崩溃,就像线程一样。我更喜欢将该模式更广泛地理解为 anti-social ,因为它不太特定于线程,因此更广泛地应用于许多编程领域。

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

第一个也是最旧的术语集无法更好地区分 thread敌意 thread兼容性。线程兼容性比所谓的线程安全性更被动,但这并不意味着所调用的代码对于并发线程使用是不安全的。这只是意味着它对允许这样做的同步是被动的,将其推迟到调用代码中,而不是将其作为内部实现的一部分来提供。 线程兼容是大多数情况下默认情况下应如何编写代码的方式,但是可悲的是,这也被错误地认为是线程不安全,就好像它是固有的反安全性一样,程序员的主要困惑点。

提醒我们的目标

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

我们通过创建我们可以依靠的确定性系统来做到这一点。确定性是昂贵的,主要是由于失去诸如并发,流水线和重新排序之类的东西的机会成本。我们试图使确定性的数量降到最低,以保持较低的成本,同时避免做出会进一步侵蚀我们所能承受的确定性的决定。

线程同步是关于增加顺序和减少混乱。执行此操作的级别对应于上述条款。最高级别意味着系统每次都以完全可预测的方式运行。第二级意味着系统表现良好,以至于调用代码可以可靠地检测出不可预测性。例如,条件变量中的 spurious 唤醒或由于未准备好互斥锁而无法锁定。第三级意味着该系统不能很好地与其他任何人一起玩,并且只能在不引起混乱的情况下单线程运行。

答案 15 :(得分:1)

相对于将 code classes 视为线程安全与否,我认为将 actions 视为线程更有益。 -安全。如果两个操作在从任意线程上下文运行时均具有指定的行为,则它们是线程安全的。在许多情况下,类将以线程安全方式支持某些动作组合,而其他组合则不支持。

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

更有趣的是,某些哈希集集合(例如.NET中原始的非泛型集合)可以保证只要没有删除任何项,并且前提是只有一个线程向它们写入,则任何线程尝试读取该集合的行为就像访问一个集合一样,在该集合中更新可能会延迟并以任意顺序发生,但在其他情况下则表现正常。如果线程1将X加Y,然后线程2寻找并先Y和X,则线程2可能会看到Y存在,但X不存在。这种行为是否是“线程安全的”取决于线程2是否准备好应对这种可能性。

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

答案 16 :(得分:0)

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

*条件适用

其他回答提到了条件 1.如果您在其上执行一个线程或多个线程等,结果应该相同。

答案 17 :(得分:0)

让我们以示例的方式回答这个问题:

class NonThreadSafe {

    private int counter = 0;

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

countTo10方法向计数器加1,然后如果计数达到10,则返回true。它应该仅返回一次true。

只要只有一个线程正在运行代码,此方法将起作用。如果两个线程同时运行代码,则会发生各种问题。

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

因此此代码不是线程安全的。

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

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