线程安全编程

7
我一直听到“线程安全”这个词。它是什么,我应该如何学习编写线程安全的代码?
此外,如果我有两个线程,一个写入一个结构体,另一个从中读取,这会有危险吗?我需要留意哪些方面呢?我认为这不是问题。两个线程没有可能同时访问结构体。
还有,请问在这个例子https://dev59.com/Km435IYBdhLWcg3w7k6b#5125493中我们如何更好地处理并发问题?我不太理解。

1
Google 上的前几个搜索结果似乎非常有指导意义。 - jogojapan
1
抱歉,我尝试过了,但我发现它太复杂了。我需要一个简单的例子和解决方法。 - Kam
2
你不需要一个简单的解决方法。你需要一个好的、全面的主题解释,这可能比任何答案都要长(尽管它们可能会提供有用的链接)。 - ugoren
2
花哨的词汇有时候是重要概念的有用简写。想象一下,如果不使用“方向盘”和“汽油”这样的术语来理解汽车,会是什么情况。线程和并发是深奥的主题,大多数人没有认真学习是无法理解的。 - djna
1
抱歉,我不同意。简单的解决方法确实存在。对于一个简单的共享结构体,您无需担心死锁(它是锁层次结构中的叶节点,无法形成循环),性能也不应受到过早优化的影响。 - MSalters
显示剩余2条评论
7个回答

7
这是一个非常深入的话题。线程通常是关于通过同时使用多个核心使事情快速运行,或在没有良好方法将操作与“主”线程交错的情况下在后台执行长时间操作的问题。后者在UI编程中非常常见。
您的场景是经典麻烦点之一,也是人们首先遇到的问题之一。拥有结构体其成员真正独立的情况非常罕见。通常希望修改结构中的多个值以保持一致性。如果没有任何预防措施,则很可能会修改第一个值,然后让另一个线程读取结构并在第二个值被写入之前对其进行操作。
简单的例子是用于2D图形的“point”结构体。您想将点从[2,2]移动到[5,6]。如果您有不同的线程绘制到该点的线条,那么很容易绘制到[5,2]。
这只是冰山一角。有很多优秀的书籍,但学习这个领域通常是这样的:
1. 哎呀,我刚刚从不一致的状态中读取了那个东西。 2. 哎呀,我刚刚从2个线程修改了那个东西,现在它是垃圾了。 3. 耶!我学会了锁 4. 哇,当我有很多嵌套代码中的锁定时,一切似乎都会卡住。 5. 嗯。我需要停止在飞行中进行这种锁定,我似乎错过了很多地方;因此我应该将它们封装在数据结构中。 6. 那个数据结构很棒,但现在我似乎一直在锁定,并且我的代码就像单个线程一样慢。 7. 条件变量很奇怪。 8. 它很快,因为我聪明地锁定了东西。嗯。有时数据会损坏。 9. 哇....InterlockedWhatDidYouSay? 10. 嘿,看啊,没有锁,我做了一个叫做自旋锁的事情。 11. 条件变量。嗯...我知道了。 12. 您知道吗,我只是开始考虑如何以完全独立的方式操作这些内容,将我的操作流水线化,并尽可能少地跨线程依赖...
显然,这不仅仅与条件变量有关。但是有许多问题可以通过线程解决,可能有几乎同样多的方法来解决它,甚至更多的方法来解决它错误。

4

线程安全是“并发编程”这个总体问题集合中的一个方面。建议您对该主题进行更多的阅读。

您认为两个线程不能同时访问结构体的假设是不正确的。首先:今天我们有多核机器,因此两个线程可以完全同时运行。其次:即使在单核机器上,分配给其他任何线程的时间片是不可预测的。您必须预期在任意时间,“其他”线程可能正在处理。请参见下面我的“机会窗口”示例。

线程安全的概念正是为了回答“这样做是否存在任何危险”的问题。关键问题是代码在一个线程中运行时是否可能得到一些数据的不一致视图,这种不一致性是由于另一个线程在更改数据时它正在运行而发生的。

在您的示例中,一个线程正在读取一个结构体,同时另一个线程正在写入。假设有两个相关字段:

  { foreground: red; background: black }

作者正在修改这些内容。

   foreground = black;
            <=== window of opportunity
   background = red;

如果读者只在这个时机读取数值,那么它会看到一个“无意义”的组合。

  { foreground: black; background: black }

这个模式的核心原理是在我们做出改变的短暂时间内,系统变得不一致,读者不应该使用这些数值。当我们完成更改后,它再次变得可读。

因此,我们使用Stefan提到的CriticalSection API来防止线程看到不一致的状态。


嗨,请查看此https://dev59.com/Km435IYBdhLWcg3w7k6b#5125493。他说这是您可以使东西线程安全的方法..我没有看到它..封装前后有什么不同? - Kam
1
这是正确的,但甚至可以更糟。我不知道这些颜色是什么类型,但如果它们不是基本原子类型,那么你实际上会在赋值本身中获得机会的窗口。 - edA-qa mort-ora-y
我只是在谈论这个确切的答案,而不是整个线程 :) - Kam
确保不使用全局变量。如果您当前有全局变量,请将它们作为每个线程状态结构体的成员,然后让线程将该结构体传递给公共函数。 - Kam
@Kam,那个答案不好。将结构体值作为参数传递并不能解决问题——在获取调用值的时间仍然存在获取不一致值的可能性。非常小心地避免并发问题,这比表面上看起来要深入得多。 - djna
@djna:我正在编辑你的答案,因为你在答案中放置的链接无法使用。如果你找到一个可用的链接,请编辑它。 - Tejus Prasad

3

那到底是什么?

简单来说,一个可以在并发环境下执行而不会出现与并发相关错误的程序。

如果ThreadA和ThreadB在没有错误的情况下读取和/或写入数据,并使用适当的同步,则该程序可能是线程安全的。这是一种设计选择--使对象线程安全可以通过多种方式实现,更复杂的类型可以使用这些技术的组合来实现线程安全。

我在哪里可以学习编写线程安全代码?

boost/libs/thread/可能是一个很好的介绍。这个主题非常复杂。

C++11标准库提供了锁、原子和线程的实现--任何使用这些的良好编写程序都是值得阅读的。标准库是模仿boost的实现而建立的。

假设我有两个线程,一个线程写入一个结构,另一个线程从中读取。这样做有什么危险吗?我应该注意什么吗?

是的,这可能是危险的和/或可能产生不正确的结果。想象一下一个线程可能在任何时候运行完毕,然后另一个线程就可以读取或修改该结构--如果您没有保护它,它可能处于更新过程中。一种常见的解决方法是锁定,可以用于防止另一个线程在读取/写入期间访问共享资源。


正如我在之前的帖子中提到的,请查看这个stackoverflow.com/a/5125493/1248779。他说这是如何使东西线程安全的方法...我没有看到它...封装前后有什么不同? - Kam
@user1248779 Theo的int myFunc(struct myState *state)如果@a state可能被另一个线程写入,则不是线程安全的。如果@a state是线程本地数据,则它将是线程安全的。换句话说,使此程序线程安全的不是封装,而是@a state指向的对象是线程本地而不是全局的。还要注意,Theo确实指定@a state是线程本地的。 - justin
那么为什么他要封装 x 和 y 并创建一个结构体?有什么好处吗?请容忍我,我真的看不出来。 - Kam
线程本地数据是重要的部分,结构并不重要。 - justin
抱歉Justin,但是什么是线程本地数据?如果你感到不安并告诉自己我的问题很荒谬,我理解并且你可以指向一本书或其他东西 :) - Kam
@Karn 我并不生气,请在提问前先使用谷歌搜索 - http://xiao-feng.blogspot.com/2008/08/what-is-thread-local-data.html - justin

1

1
线程安全是一个简单的概念:在一个线程执行操作A的同时,另一个线程是否可以执行操作B,这可能与操作A相同或不同。这可以扩展到涵盖许多线程。在这种情况下,“安全”意味着:
  • 没有未定义的行为
  • 数据结构的所有不变量都由线程保证被观察到
实际的操作A和B非常重要。如果两个线程都读取一个普通的int变量,那么这是可以的。然而,如果任何线程可能写入该变量,并且没有同步来确保读取和写入不能同时发生,则会出现数据竞争,这是未定义的行为,也不是线程安全的。
这同样适用于您所询问的场景:除非您采取了特殊预防措施,否则在一个线程读取结构的同时,另一个线程写入它是不安全的。如果您可以通过某种形式的同步(如互斥锁、临界区、信号量或事件)保证线程不能同时访问数据结构,则没有问题。

您可以使用诸如互斥锁和临界区等工具来防止对某些数据的并发访问,以便在写入线程正在写入数据时,只有该线程访问数据,而在读取线程正在读取数据时,只有该线程访问数据,从而提供了我刚才提到的保证。因此,这避免了上述提到的未定义行为。

但是,您仍然需要确保您的代码在更广泛的上下文中是安全的:如果您需要修改多个变量,则需要在整个操作期间持有互斥锁的锁定,而不是每个单独访问都需要锁定,否则您可能会发现其他线程无法观察到数据结构的不变量。

还有可能出现一种情况,即某个数据结构对于某些操作是线程安全的,但对于其他操作则不是。例如,单生产者单消费者队列将在一个线程将项目推送到队列中,另一个线程将项目从队列中弹出时正常工作,但如果两个线程都在推送项目或两个线程都在弹出项目,则会出现问题。

在你提到的例子中,关键在于全局变量隐式共享在所有线程之间,因此所有访问都必须受到某种形式的同步保护(如互斥锁),如果任何一个线程可以修改它们。另一方面,如果每个线程都有一个单独的数据副本,则该线程可以修改其副本而不必担心来自任何其他线程的并发访问,并且不需要同步。当然,如果两个或多个线程将操作相同的数据,则始终需要同步。
我的书 《C++并发编程实战》 讲解了什么是线程安全,如何设计线程安全的数据结构以及用于此目的的C++同步原语,例如 std::mutex

0
回答问题的第二部分:想象一下两个线程都访问std::vector<int> data
//first thread
if (data.size() > 0)
{
   std::cout << data[0]; //fails if data.size() == 0
}

//second thread
if (rand() % 5 == 0)
{
   data.clear();
}
else
{
   data.push_back(1);
}

将这些线程并行运行,你的程序会崩溃,因为std::cout << data[0];可能会在data.clear();之后直接执行。

你需要知道,在你的线程代码的任何时刻,线程都可能被中断,例如在检查(data.size() > 0)之后,另一个线程可能变得活跃。虽然第一个线程在单线程应用程序中看起来是正确的,但在多线程程序中却不是。


0

线程安全是指某个代码块受到保护,以防止多个线程访问。这意味着被操作的数据始终保持一致的状态。

一个常见的例子是生产者消费者问题,其中一个线程从数据结构中读取,而另一个线程向同一数据结构写入:详细解释


这不是关于保护一块代码,而是关于保护一块数据。 - Alexey Frunze
1
@Alex,实际上两者都是。一旦涉及到设备(即使只是控制台、窗口或文件),即使您没有修改自己程序中的任何数据,您仍然可能需要锁定。 - edA-qa mort-ora-y

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