关于多线程:“线程安全”代码是什么意思?

What is meant by “thread-safe” code?

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

  • 刚刚看到一个关于这个问题的有趣的讨论:blogs.msdn.com/ericlippert/archive/2009/10/19/…


维基百科:

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

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

重新进入:

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

相互排斥:

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

线程本地存储:

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

原子操作:

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

阅读更多:

http://en.wikipedia.org/wiki/thread_安全

  • 德语:网址:http://de.wikipedia.org/wiki/threadsicherheit

  • 法语:网址:http://fr.wikipedia.org/wiki/threasafe(非常短)

  • 描述性回答。其他大多数答案也是正确的。谢谢大家
  • 从技术上讲,这一联系缺少几个关键点。所讨论的共享内存必须是可变的(只读内存不能是线程不安全的),并且多线程必须,a)在内存处于不一致(错误)状态的中间期间对内存执行多个写操作,b)允许其他线程在内存不一致时中断该线程。
  • 当搜索谷歌的第一个结果是wiki时,没有必要在这里重复它。
  • "访问函数的代码"是什么意思?正在执行的函数本身就是代码,不是吗?


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

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

  • 在同一个过程中!
  • 事实上,在同样的过程中:)
  • "写几个星期稳定运行的代码需要极度的偏执。"这是我喜欢的一句话:)
  • 啊!这个答案只是重申了这个问题!——为什么只有在同一个过程中????如果代码在多个线程从不同的进程执行时失败,那么,可以说,("共享内存"可能在磁盘文件中),它不是线程安全的!!
  • 请注意,这里@charlesbretana使用的是一个更概念化的(和直观的?)"线程"的定义,以涵盖不涉及实际线程的潜在多处理方案。(在Python中,有完整的框架可以做到这一点,不需要线程或共享内存/磁盘,而是通过将pickled对象作为消息传递。)
  • "乔恩,…我假设,任何时候,只要一个代码块在另一个线程可以访问(并且可以修改)的资源中创建一个不变量,无论该另一个线程是在同一个进程中执行还是在另一个进程中执行,我们都是"多线程"。除非显式编码不为,否则所有现代操作系统都可以(和确实)中断运行代码来实现多重处理。
  • @如果代码(段)失败,如果许多线程在不同的进程中同时执行它,那么如果每个进程有一个线程执行它,并且这些进程同时运行,那么代码(段)将失败,这样代码(段)就不缺少线程安全性,而只是不安全。
  • @MG30RG,如果它"在每个进程执行一个线程并且这些进程同时运行时失败",那么它不是"线程安全的"。如果一个线程只在一个进程中执行它时失败,那么您的语句就是真的,那么它就是不安全的。如果在多个线程同时执行时失败(不是"同时"),但在一个并发线程中执行时不能遇到相同的失败,那么它是线程不安全的。
  • @ MG30RG。也许这种混淆是由于不知何故地认为,当一个代码块由多个进程执行,但每个进程只有一个线程执行时,不知何故,它仍然是一个"单线程"场景,而不是多线程场景。这个想法甚至没有错。这只是错误的定义。显然,多个进程通常不会以同步的方式在同一线程上执行(除非在少数情况下,通过设计的进程相互协调,并且操作系统在进程之间共享线程)。
  • @Charlesbretana"很明显,多个进程通常不会以同步方式在同一线程上执行",我认为此语句仅在Windows(或更普遍的多线程)环境中有效,但在Linux(或更普遍的多进程)环境中无效,因为后者倾向于分发批处理进程之间的ED问题,而不是线程。discalimer:在这两个提到的操作系统上,多处理和多线程都是可用的,只是它们在我处理它们的方式上表现得更好。NO-火焰原子吸收光谱法
  • @mrg30rg,如果Linux倾向于"…在进程之间分发批处理问题,而不是在线程之间分发。…"然后它们在不同的线程上。两个不同的进程如何只在一个线程上运行?除非它们以某种方式同步(比如在只有一个CPU的机器上运行多进程操作系统),否则它们不能。但是在这种情况下,所有运行的单个"线程"是一个比我们讨论的线程更高级别的逻辑抽象。


一个更具信息性的问题是什么使得代码不安全——答案是有四个条件必须是正确的……想象一下下面的代码(它是机器语言翻译)

1
2
3
4
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必须准确地表示任何线程执行increment语句任何部分的总次数"。通常,此不变属性需要保持为真(在本例中,totalRequests必须保持准确的计数),然后才能进行更新以使更新正确。
  • 第三个条件是,在实际更新的某些部分中,不变量属性不保持不变。(在处理的某些部分中,它暂时无效或错误)。在这种特殊情况下,从获取totalrequests到存储更新值,totalrequests不满足不变量。
  • 第四个也是最后一个必须发生的竞争条件(以及代码因此而不是"线程安全")是另一个线程必须能够在不变量被破坏时访问共享内存,从而导致不一致或不正确的行为。
    • 这只涉及所谓的数据竞赛,当然也很重要。然而,还有其他一些方法无法保证代码的线程安全,例如错误的锁定可能导致死锁。甚至一些简单的东西,比如Java线程中的某个调用系统(EXIT),使代码不是线程安全的。
    • 我想在某种程度上这是语义上的,但我认为错误的锁定代码会导致死锁,并不会使代码不安全。首先,不需要首先锁定代码,除非可能存在如上所述的竞争条件。然后,如果编写锁定代码的方式导致死锁,这不是线程不安全的,而是错误的代码。
    • 但是请注意,死锁不会在运行单线程时发生,因此对于我们大多数人来说,这肯定是在"线程安全"的直观含义下发生的。
    • 当然,除非您运行的是多线程的,否则死锁不会发生,但这就像是说,如果您在一台计算机上运行,网络问题就不会发生。如果程序员编写代码以便在完成更新之前从代码的关键行中分离出来,并修改其他子例程中的变量,那么其他问题也可能发生在单线程中。


    我喜欢Brian Goetz的Java并发性在实践中的定义,因为它的全面性。

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

    • 这个定义不完整,不具体,绝对不全面。它必须安全运行几次,就一次?十次?每一次?80%的时间?它没有具体说明什么使它"不安全"。如果它不能安全运行,但是失败是因为有一个除以零的错误,这会使它线程"不安全"吗?
    • 没有99.99999%的线程安全代码。
    • 下次要文明一点,也许我们可以讨论一下。这不是Reddit,我也没有心情和粗鲁的人交谈。
    • 你把别人的定义解释为对你自己的侮辱,这是在说。在情绪化反应之前,你需要阅读和理解物质。我的评论没有任何不文明之处。我对这个定义的含义提出了一点看法。对不起,如果我用来说明这一点的例子让你感到不舒服。


    正如其他人所指出的,线程安全意味着如果一个以上的线程同时使用一段代码,那么它将不会出错。

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

    例如,Java有两个几乎相等的类,EDCOX1,0,EDCX1,1。区别在于StringBuffer是线程安全的,因此多个线程可以同时使用StringBuffer的单个实例。StringBuilder不是线程安全的,当字符串仅由一个线程构建时,它被设计为对这些情况(绝大多数情况)的更高性能替换。

    • 从Java背景来看,这是非常清楚的。


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


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

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

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


    是和不是。

    线程安全不仅仅是确保共享数据一次只能被一个线程访问。您必须确保对共享数据的顺序访问,同时避免争用条件、死锁、LiveLocks和资源匮乏。

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


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

    在实践中,我喜欢Java并发给出的定义:

    A [portion of code] is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.

    正确地说,它们意味着程序的行为符合其规范。

    人为的例子

    假设您实现了一个计数器。你可以说它的行为是正确的,如果:

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

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

    注:Cross Post on Programmers


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


    我想在其他好答案的基础上再添加一些信息。

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

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

    螺纹安全是什么意思?

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

    从高级并发API上的Oracle文档页面:

    内存一致性属性:

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

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

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

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

  • 不要混淆线程安全和确定性。线程安全代码也可以是非确定性的。考虑到用线程代码调试问题的困难,这可能是正常情况。-)

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


    完成其他答案:

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

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

    线程调度不能保证是循环调度。一个任务可能会以牺牲相同优先级的线程为代价完全占用CPU。您可以使用thread.yield()来问心无愧。可以使用(在Java中)线程。StestPosits(Trime.NoMyPrRoRITY-1)来降低线程的优先级。

    谨防:

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

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

    请注意,结果的不可预测性是"竞态条件"的结果,这可能导致数据按预期顺序之外的顺序进行修改。


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

    1
    2
    3
    4
    5
    6
    7
    8
    class NonThreadSafe {

        private int counter = 0;

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

    countTo10方法向计数器中添加一个,如果计数达到10,则返回true。它应该只返回一次真值。

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

    例如,如果count以9开始,一个线程可以添加1到count(生成10),但第二个线程可以进入该方法,并在第一个线程有机会执行与10的比较之前再次添加1(生成11)。然后两个线程进行比较,发现count为11,两者都不返回true。

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

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


    简单来说:p如果在代码块上执行多个线程是安全的,那么它是线程安全的。*

    *条件适用

    其他回答如1。如果您在它上面执行一个线程或多个线程等,结果应该是相同的。