线程安全编程

Thread safe programming

本文关键字:编程 安全 线程      更新时间:2023-10-16

我一直听说线程安全。这到底是什么?我如何以及在哪里可以学习编写线程安全代码?

另外,假设我有两个线程,一个写结构,另一个读结构。这有什么危险吗?有什么我要找的吗?我不认为这是个问题。两个线程不会(也不可能)同时访问结构。。

另外,有人能告诉我在这个例子中如何:https://stackoverflow.com/a/5125493/1248779我们在并发问题上做得更好。我不明白。

这是一个非常深入的话题。核心线程通常是通过同时使用多个核心来使事情进展得更快;或者当您没有一个好的方法将操作与"主"线程交错时,在后台执行长操作。后者在UI编程中非常常见。

你的场景是典型的麻烦点之一,也是最早遇到的人之一。很少有成员真正独立的结构。想要修改结构中的多个值以保持一致性是非常常见的。如果不采取任何预防措施,很有可能修改第一个值,然后让另一个线程读取结构并在写入第二个值之前对其进行操作。

一个简单的例子是2d图形的"点"结构。您希望将点从[2,2]移动到[5,6]。如果你用另一根线画一条线到那个点,你可以很容易地画到[5,2]。

这真的只是冰山一角。有很多很棒的书,但学习这个领域通常是这样的:

  1. 哦,我只是在不一致的状态下读到了那篇文章
  2. 哦,我刚刚从两个线程修改了那个东西,现在它是垃圾
  3. 耶!我学会了锁
  4. 哇,我有很多锁,当我在嵌套代码中有很多锁时,有时一切似乎都挂起了
  5. 小时。我需要停止这种快速锁定,我似乎错过了很多地方;所以我应该把它们封装在一个数据结构中
  6. 数据结构很好,但现在我似乎一直在锁定,我的代码就像一个线程一样慢
  7. 条件变量很奇怪
  8. 它很快,因为我在锁东西方面很聪明。小时。有时数据会损坏
  9. 哇。。。。联锁你说了什么
  10. 嘿,看,没有锁,我做的这个东西叫旋转锁
  11. 条件变量。小时。。。我明白了
  12. 你知道吗,不如我开始思考如何以完全独立的方式操作这些东西,流水线化我的操作,并尽可能少地使用跨线程依赖关系

显然,这并不全是关于条件变量。但是,有很多问题可以通过线程来解决,可能有同样多的方法可以解决,甚至有更多的方法可以出错。

线程安全是"并发编程"总标题下一组更大问题的一个方面。我建议围绕这个主题阅读。

假设两个线程不能同时访问结构是不好的。第一:今天我们有多核机器,所以两个线程可以同时运行。第二:即使在单核机器上,分配给任何其他线程的时间片段也是不可预测的。您必须预测蚂蚁在任何"其他"线程可能正在处理的任意时间。请参阅下面的"机会之窗"示例。

线程安全的概念正是为了回答"这在任何方面都危险吗"的问题。关键的问题是,在一个线程中运行的代码是否有可能获得某些数据的不一致视图,这种不一致的发生是因为它在运行另一个线程时正在更改数据。

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

  { foreground: red; background: black }

而作者正在更改那些

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

如果读者在这个机会窗口读取值,那么它就会看到一个"无稽之谈"的组合

  { foreground: black; background: black }

这种模式的本质是,在很短的一段时间内,当我们进行更改时,系统变得不一致,读者不应该使用这些值。一旦我们完成更改,就可以安全地再次阅读。

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

那到底是什么?

简单地说,一种可以在并发上下文中执行而没有与并发相关的错误的程序。

如果ThreadA和ThreadB读取和/或写入数据时没有错误,并且使用了正确的同步,那么程序可能是线程安全的。这是一种设计选择——使对象线程安全可以通过多种方式实现,而使用这些技术的组合,更复杂的类型可能是线程安全的。

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

boost/libs/thread/可能是一个很好的介绍。这个题目相当复杂。

C++11标准库提供了锁、原子和线程的实现——任何使用这些的编写良好的程序都是一本不错的读物。标准库是根据boost的实现进行建模的。

另外,假设我有两个线程,一个写结构,另一个读结构。这有什么危险吗?有什么我要找的吗?

是的,它可能是危险的和/或可能产生错误的结果。想象一下,一个线程可能在任何时候都会耗尽时间,然后另一个线程可以读取或修改该结构——如果你没有保护它,它可能正在更新中。一个常见的解决方案是锁,它可以用来防止另一个线程在读/写期间访问共享资源。

在WIN32平台上编写多线程C++程序时,需要保护某些共享对象,以便在任何给定时间只有一个线程可以从不同的线程访问它们。您可以使用5种系统功能来实现这一点。它们是InitializeCriticalSection、EnterCriticalSection,TryEnterCriticalSection、LeaveCriticalSection和DeleteCriticalSection。

此外,也许这些链接可以帮助:如何使应用程序线程安全?

http://www.codeproject.com/Articles/1779/Making-your-C-code-thread-safe

线程安全是一个简单的概念:在一个线程上执行操作a,而另一个线程正在执行操作B,这可能与操作a相同,也可能不同。这可以扩展到覆盖许多线程。在这种情况下,"安全"意味着:

  • 没有未定义的行为
  • 数据结构的所有不变量都保证被线程观察到

实际操作A和B很重要。如果两个线程都读取一个普通的int变量,那么这是可以的。但是,如果任何线程可以向该变量写入,并且没有同步来确保读取和写入不能同时发生,则存在数据竞争,这是未定义的行为,这是线程安全的。

这同样适用于您询问的场景:除非您采取了特别的预防措施,否则让一个线程在另一个线程写入结构的同时从结构中读取是不安全的,关键部分、信号量或事件,则没有问题。

您可以使用互斥和关键部分之类的东西来防止对某些数据的并发访问,这样写线程在写数据时是唯一访问数据的线程,读线程在读数据时是惟一访问数据的螺纹,从而提供了我刚才提到的保证。因此,这避免了上述未定义的行为。

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

数据结构也可能对某些操作是线程安全的,但对其他操作则不是。例如,如果一个线程在队列中推送项目,而另一个线程从队列中弹出项目,则单个生产者-单个消费者队列将正常,但如果两个线程在推送项目或两个线程正在弹出项目,将中断。

在您引用的示例中,重点是全局变量在所有线程之间隐式共享,因此,如果任何线程都可以修改全局变量,则所有访问都必须通过某种形式的同步(如互斥)进行保护。另一方面,如果每个线程都有一个单独的数据副本,那么该线程可以修改其副本,而不必担心来自任何其他线程的并发访问,并且不需要同步。当然,如果两个或多个线程要对同一数据进行操作,则总是需要同步。

我的书《C++并发在行动》涵盖了线程安全意味着什么,如何设计线程安全的数据结构,以及用于此目的的C++同步原语,如std::mutex

线程安全是指保护某个代码块不被多个线程访问。这意味着被操纵的数据始终处于一致状态。

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

回答问题的第二部分:想象两个线程都访问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)之后,另一个线程可能会变为活动线程。虽然第一个线程在单线程应用程序中看起来是正确的,但它不在多线程程序中。