术语“线程安全”的含义是什么?


Answers:


256

从维基百科:

线程安全是一种适用于多线程程序上下文的计算机编程概念。如果一段代码在多个线程同时执行期间正常运行,则它是线程安全的。特别是,它必须满足多个线程访问同一共享数据的需求,以及在任何给定时间仅由一个线程访问共享数据的需求。

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

重入:

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

互斥:

使用确保没有线程在任何时间读取或写入共享数据的机制来串行化对共享数据的访问。如果一段代码访问多个共享的数据,则需要格外小心-问题包括竞争状况,死锁,活锁,饥饿以及许多操作系统教科书中列举的各种其他弊病。

线程本地存储:

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

原子操作:

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

阅读更多:

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



4
从技术上讲,此链接缺少几个关键点。有问题的共享内存必须是可变的(只读内存不能是线程不安全的),并且多个线程必须:a)对内存执行多次写操作,在此期间内存不一致(错误) b)允许其他线程在内存不一致时中断该线程。
查尔斯·布雷塔纳

20
当Google搜索的第一个结果是Wiki时,就没有必要在此处使其多余。
Ranvir

您是什么意思“代码访问函数”?被执行的功能本身就是代码,不是吗?
Koray Tugay 2015年

82

线程安全代码是即使许多线程同时执行也可以运行的代码。

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


34
在同一过程中!
阿里·阿夫沙尔

实际上,以相同的过程:)
Marek Blotny

4
“编写能够稳定运行数周的代码会产生极大的偏执。” 我喜欢那句话:)
Jim T

5
h!这个答案只是重申了问题!---为什么只在同一过程中???如果当多个线程从不同进程执行代码时代码失败,那么可以说(“共享内存”可能在磁盘文件中),这不是线程安全的!
查尔斯·布雷塔纳

1
@ mg30rg。可能是由于某种方式认为当一个代码块正在由多个进程执行,而每个进程仅由一个线程执行时,这种困惑的结果是,某种程度上,这仍然是“单线程”方案,而不是多线程方案。这个想法甚至没有错。这只是误解。显然,多个进程通常不会以同步的方式在同一个线程上执行(除非在极少数情况下,设计使进程相互协调,并且OS在进程之间共享线程。)
Charles Bretana

50

一个更具信息量的问题是什么使代码不具有线程安全性,并且答案是必须满足四个条件……想象以下代码(它是机器语言翻译)

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. 第二个条件是存在与这些共享内存位置相关联的属性(通常称为invariant),该属性必须为true或有效,程序才能正常运行。在上面的示例中,属性是“ totalRequests必须准确表示任何线程执行增量语句的任何部分的总次数 ”。通常,在发生更新以使更新正确之前,此不变属性需要保持为true(在这种情况下,totalRequests必须保留准确的计数)。
  3. 第三个条件是,在实际更新的某些部分中,invariant属性不成立。(在处理的某些部分暂时无效或为假)。在这种特定情况下,从提取totalRequests的时间到存储更新值的时间,totalRequests 不能满足不变式。
  4. 对于比赛发生必须发生的第四个也是最后条件(并为代码,因此被“线程安全”)是另一个线程必须能够访问共享内存,而不变被打破,从而导致不一致或错误行为。

6
这仅涵盖所谓的数据竞赛,当然很重要。但是,还有其他方式使代码无法保证线程安全-例如,错误的锁定可能导致死锁。甚至像在Java线程中某个地方调用System.exit()之类的简单操作也使该代码不安全。
Ingo

2
我猜这在某种程度上是语义,但是我认为可以导致死锁的错误锁定代码不会使代码不安全。首先,没有必要首先将代码锁定,除非如上所述的竞争条件是可能的。然后,如果您以导致死锁的方式编写锁定代码,这不是线程不安全的,那只是不好的代码。
查尔斯·布雷塔纳

1
但是请注意,在单线程运行时不会发生死锁,因此对于我们大多数人来说,这肯定属于(不是)“线程安全”的直观含义。
乔恩·库姆斯

好吧,除非您正在运行多线程,否则就不会发生死锁,但这就像说如果在一台计算机上运行就不会发生网络问题。如果程序员编写代码以便在完成更新之前修改关键代码行,并在其他子例程中修改变量,则单线程也会发生其他问题。
查尔斯·布雷塔纳'16

34

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

“如果一个类在从多个线程访问时能正确运行,则该线程是线程安全的,而不管运行时环境对那些线程的执行进行调度或交织,并且调用代码部分没有其他同步或其他协调。 ”


此定义不完整且不具体,绝对不全面。它必须安全运行多少次,仅一次?十次?每次?80%的时间?并且未指定是什么使它“不安全”。如果它不能安全运行,但是失败是因为存在除以零的错误,是否使它成为线程“不安全”?
Charles Bretana

下次再讲文明吧,也许我们可以讨论。这不是Reddit,我也不想和粗鲁的人聊天。
Buu Nguyen

您对别人定义的解释评论是对自己的侮辱。您需要在情感上做出反应之前阅读和理解内容。我的评论丝毫不逊色。我提出了定义的含义。抱歉,如果我用来说明问题的示例使您感到不舒服。
Charles Bretana

28

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

值得一提的是,这有时会付出一定的代价,不仅要花费计算机时间,而且要编写更复杂的代码,所以这并不总是令人满意的。如果一个类只能在一个线程上安全地使用,则最好这样做。

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


22

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


21

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

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

  • 由共享变量的相互依赖性导致的死锁
    如果您有两个共享变量A和B。在一个函数中,先锁定A,然后再锁定B。在另一个函数中,开始锁定B,过一会儿再锁定A。是一个潜在的死锁,当第二个功能将等待A解锁时,第一个功能将等待B解锁。此问题可能不会在您的开发环境中发生,并且只会不时出现。为了避免这种情况,所有锁必须始终处于相同顺序。


9

是的,没有。

线程安全不仅是确保一次仅一个线程访问您的共享数据,还有一点点。您必须确保顺序访问共享数据,同时避免争用情况死锁活动锁资源匮乏

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


9

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

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

如果[代码部分]从多个线程访问时行为正确,则无论线程在运行时环境中对这些线程的执行进行调度或交织,并且在执行该操作时,也无需进行其他同步或其他协调,则该代码部分是线程安全的调用代码。

通过正确的他们是指以符合其规范的程序的行为。

人为的例子

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

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

不管有多少线程并发访问它,线程安全计数器都将根据这些规则进行操作(天真的实现通常不会这样)。

注意:关于程序员的交叉文章



5

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

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


5

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

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

请查看此SE问题以获取更多详细信息:

线程安全是什么意思?

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

在高级并发API的oracle文档页面上:

内存一致性属性:

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

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

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

  1. 在将对象放入任何并发集合之前,线程中的操作发生在访问另一个线程中的元素或从集合中删除该元素之后的操作。
  2. 操作在提交的前一个线程RunnableExecutor发生-before其执行开始。提交给的Callables同样如此ExecutorService
  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(以及其变体)发生-前行动由阻挡动作执行,并且操作由阻挡动作进行发生-before随后通过从在其他线程对应AWAIT成功返回动作。

4

要完成其他答案:

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

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

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

线程调度不保证是循环的。一个任务可能会完全占用CPU,但会消耗相同优先级的线程。您可以使用Thread.yield()获得良心。您可以使用(在Java中)Thread.setPriority(Thread.NORM_PRIORITY-1)降低线程的优先级

另外请注意:

  • 遍历这些“线程安全”结构的应用程序的运行时成本很高(其他人已经提到过)。
  • Thread.sleep(5000)应该休眠5秒。但是,如果有人更改了系统时间,您可能会睡很长时间或根本没有时间。操作系统以绝对形式而不是相对形式记录唤醒时间。

2

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

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


2

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

class NonThreadSafe {

    private int counter = 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。

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

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

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


1

至少在C ++中,我认为线程安全有点用词不当,因为它与名字无关。为了保持线程安全,代码通常必须对此具有主动性。通常这不是一种被动的品质。

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

这就是为什么有些人喜欢使用内部同步一词的原因。

术语集

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

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

第二个(更好)是:

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

第三个是:

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

类比

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

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

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

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

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

不是线程安全的(而且很糟糕)〜线程具有敌意不可同步

线程敌对系统的日常比喻的一个例子是,一辆跑车拒绝使用他们的方向盘,故意随意改变车道,这有点混蛋。他们的驾驶风格是敌对的无法同步的,因为您无法与他们协调,这可能导致争夺同一车道而没有解决方案,从而导致事故,因为两辆汽车试图占用相同的空间而没有任何协议防止这种情况。也可以将这种模式更广泛地视为反社交模式,我更喜欢这种模式因为它不特定于线程,因此更广泛地应用于许多编程领域。

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

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

注意:许多软件手册实际上使用“线程安全”一词来指代“线程兼容”,这使本来一团糟的事情更加混乱!出于这个原因,我不惜一切代价避免使用“线程安全”和“线程不安全”一词,因为有些资源会称其为“线程安全”,而另一些资源则称其为“线程不安全”,因为他们不同意关于您是否必须满足一些额外的安全标准(同步原语),或者只是怀有敌意,不被视为“安全”。因此,请避免使用这些术语,而应使用更智能的术语,以避免与其他工程师进行危险的误解。

提醒我们的目标

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

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

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


1

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

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

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

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


0

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

*条件适用

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

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.