什么是线程安全?

156

最近我尝试从一个线程(非UI线程)访问文本框,结果抛出了异常。它说什么关于“代码不是线程安全的”,所以我最终编写了一个委托(从MSDN示例中获取)并调用它来代替。

但即便如此,我还是不太明白为什么需要所有这些额外的代码。

更新: 如果我进行检查,会遇到任何严重问题吗?

Controls.CheckForIllegalCrossThread..blah =true

5
通常,“线程安全”意味着使用该术语的人认为它意味着什么,至少对于该人来说是这样。因此,在讨论线程化代码的行为时,它不是非常有用的语言结构 - 您需要更加具体明确。 - anon
8
重复的问题?:https://dev59.com/93VC5IYBdhLWcg3wihqv 的意思是什么? - Dave O.
@dave 抱歉,我尝试过搜索,但最终放弃了...无论如何还是谢谢。 - Vivek Bernard
1
一个不会出现“竞态条件”的代码 - Muhammad Babar
12个回答

161
Eric Lippert 在他的一篇博客文章中,名为“什么是你所谓的‘线程安全’?”,探讨了维基百科中关于线程安全的定义。
从这些链接中提取出了3个重要的内容:

“如果在多线程同时执行时代码正确地运行,则该代码是线程安全的。”

“特别地,它必须满足多个线程访问同一共享数据的需求,…”

“…以及同一时间只能由一个线程访问共享数据的需求。”

绝对值得一读!

34
请避免仅提供链接的答案,因为它可能在未来任何时候变得不好。 - akhil_mittal
1
更新链接:https://learn.microsoft.com/en-nz/archive/blogs/ericlippert/what-is-this-thing-you-call-thread-safe - Ryan Buddicom

142

在最简单的术语中,线程安全意味着可以安全地从多个线程访问它。当您在程序中使用多个线程并且它们每个都尝试访问共同的数据结构或内存位置时,可能会发生一些不好的事情。因此,您需要添加一些额外的代码来防止这些不良后果。例如,如果两个人同时写同一份文档,第二个人保存将覆盖第一个人的工作。为了使其线程安全,您必须强制第二个人等待第一个人完成任务,然后才允许第二个人编辑该文档。


16
这被称为同步。对吗? - JavaTechnical
3
可以使用同步技术来强制不同的线程等待访问共享资源。 - Vincent Ramdhanie
从格雷戈里的被接受的答案中,他说:“如果一段代码在多个线程同时执行时能够正确地运行,则它是线程安全的。”而你却说:“要使其线程安全,你必须强制第一个人等待”;难道他不是在说同时执行是可以接受的,而你却说不行吗?你能解释一下吗? - mfaani
这是同一件事。我只是举例说明一个简单的机制,它可以使代码具备线程安全性。无论使用何种机制,多个线程运行相同的代码时都不应该相互干扰。 - Vincent Ramdhanie
那么这只适用于使用全局和静态变量的代码吗?以您编辑文档的示例为例,我想防止第二个人在另一个文档上运行编写文档的代码是没有意义的。 - Aaron Franke
显示剩余2条评论

21

维基百科有一篇关于线程安全的文章。

这个定义页面(你需要跳过一个广告-抱歉)给出了如下定义:

在计算机编程中,线程安全描述了程序部分或例程可以从多个编程线程中调用,而不会发生线程之间的意外交互。

线程是程序的执行路径。单线程程序只有一个线程,因此这个问题不会出现。几乎所有GUI程序都有多个执行路径和线程——至少有两个,一个用于处理GUI的显示和处理用户输入,另一个用于实际执行程序操作。

这样做是为了使界面在程序工作时仍然响应,通过将任何长时间运行的进程卸载到任何非UI线程中。这些线程可能一次创建并存在于程序的整个生命周期中,也可能在需要时创建并在完成后销毁。

由于这些线程经常需要执行常见操作——磁盘I / O、向屏幕输出结果等——因此代码的这些部分需要以这样的方式编写,以便它们可以处理从多个线程同时调用它们的情况。这将涉及以下内容:

  • 使用数据的副本进行操作
  • 在关键代码周围添加锁
  • 以适当的方式打开文件——因此如果只是读取,不要为写入打开文件。
  • 应对没有访问资源的情况,因为它们被其他线程/进程锁定。

17

简单地说,线程安全指的是一个方法或类实例可以同时被多个线程使用而不会发生任何问题。

考虑下面的方法:

private int myInt = 0;
public int AddOne()
{
    int tmp = myInt;
    tmp = tmp + 1;
    myInt = tmp;
    return tmp;
}

现在线程A和线程B都想执行AddOne()方法。但是A先开始并将myInt(0)的值读入到tmp中。然后由于某些原因,调度程序决定停止线程A并延迟执行线程B。现在线程B也将myInt的值(仍为0)读入其自己的变量tmp中。线程B完成整个方法,因此最终myInt = 1,并返回1。现在又轮到线程A了。线程A继续执行。将1加到tmp中(对于线程A,tmp为0)。然后将这个值保存在myInt中。myInt再次为1。

因此,在这种情况下,AddOne()方法被调用两次,但由于该方法没有以线程安全的方式实现,因此myInt的值不是预期的2,而是1,因为第二个线程在第一个线程完成更新之前读取了变量myInt的值。

在非平凡情况下创建线程安全的方法非常困难。有很多技术可供选择。在Java中,可以将方法标记为synchronized,这意味着在给定时间只有一个线程可以执行该方法,其他线程等待。这使得方法线程安全,但如果方法中有很多工作要做,则会浪费很多空间。另一种技术是通过创建锁或信号量,并锁定此小部分(通常称为关键部分),将“仅将方法的一小部分标记为同步”。甚至有一些方法是实现为无锁线程安全的,这意味着它们构建成这样,多个线程可以同时通过它们而不会导致问题,例如当方法只执行一个原子调用时。原子调用是不能被打断且只能由一个线程同时完成的调用。


如果方法AddOne被调用了两次 - Sujith PS

12

让我们举一个现实世界的例子来解释给外行人听。

假设你有一个银行账户,账户里只有100美元。 你向你兄弟的账户转账50美元,与此同时,你的配偶正在使用同一个银行账户购物,花费了80美元。 如果这个银行账户不是线程安全的,那么银行就犯了一个很大的错误,让你和你的配偶同时进行了两笔交易,然后银行将破产!

银行账户是“共享状态”,而你和你的配偶是尝试对共享状态进行写操作的“两个不同的线程”。

线程安全意味着多个线程无法同时访问对象的状态,只有一个线程会首先执行写/读操作,然后下一个线程才能按顺序访问。

所以要么你,要么你的配偶会成功地进行第一笔交易,但不能同时进行,银行账户是线程安全的。


7
您可以从书籍《Java并发编程实践》中获取更详细的解释:
如果一个类可以在多个线程中正确地访问而不需要额外的同步或其他协调方式,无论线程的调度或交错执行如何,并且不管运行时环境如何,那么这个类就是线程安全的。

5

如果一个模块可以在多线程和并发使用的情况下保持其不变性,则该模块是线程安全的。

在这里,模块可以是数据结构、类、对象、方法/过程或函数。基本上是有作用域的代码段和相关的数据。

该保证可能仅限于某些环境,例如特定的CPU架构,但必须对这些环境保持。如果没有明确界定环境,则通常认为它适用于可以编译和执行代码的所有环境。

线程不安全的模块可能在多线程和并发使用下正常运行,但这往往更多地依赖于运气和巧合,而不是仔细的设计。即使某个模块在您的环境下不会出错,将其移动到其他环境可能会导致错误。

多线程错误通常很难调试。其中一些错误只会偶尔发生,而其他错误则表现得非常明显 - 这也可能是特定于环境的。它们可能以微妙错误的结果、死锁的形式出现。它们可能以不可预测的方式混乱数据结构,并导致其他看似不可能的错误出现在代码的其他远程部分。它可能非常应用程序特定,因此很难进行一般描述。


4
线程安全:线程安全的程序可以保护其数据免受内存一致性错误的影响。在高并发程序中,线程安全的程序不会因为多个线程对同一对象进行读取/写入操作而产生任何副作用。不同的线程可以共享和修改对象数据,而不会出现一致性错误。
您可以通过使用先进的并发API来实现线程安全。此文档 页面 提供了良好的编程结构以实现线程安全。 锁对象 支持锁定惯用语,简化了许多并发应用程序。 执行器 定义了一个高级API,用于启动和管理线程。由java.util.concurrent提供的执行器实现提供适用于大型应用程序的线程池管理。 并发集合 可以更轻松地管理大量数据,并且可以大大减少同步的需求。

原子变量 具有最小化同步和避免内存一致性错误的特性。

ThreadLocalRandom(在JDK 7中)可为多个线程提供有效的伪随机数生成。

还可以参考java.util.concurrentjava.util.concurrent.atomic包以获取其他编程结构。


2
生成线程安全代码的关键在于管理对共享可变状态的访问。当可变状态被发布或在线程之间共享时,它们需要进行同步以避免出现诸如竞争条件内存一致性错误等错误。
我最近写了一篇关于线程安全的博客,您可以阅读以获取更多信息。

1

我认为http://en.wikipedia.org/wiki/Reentrancy_%28computing%29的概念通常被视为不安全的线程,这是指一个方法具有并依赖于副作用,例如全局变量。

例如,我曾经看到过一些代码将浮点数格式化为字符串,如果在不同的线程中运行两个这样的代码,则decimalSeparator的全局值可能会永久更改为“。”

//built in global set to locale specific value (here a comma)
decimalSeparator = ','

function FormatDot(value : real):
    //save the current decimal character
    temp = decimalSeparator

    //set the global value to be 
    decimalSeparator = '.'

    //format() uses decimalSeparator behind the scenes
    result = format(value)

    //Put the original value back
    decimalSeparator = temp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接