线程同步-The Boost C++ Libraries

前言

The Boost C++ Libraries
本博客是Synchronizing Threads的一篇译文。关于《The Boost C++ Llibraries》一书的在线完整书的目录,参见The Boost C++ Libraries,Boost库的官网地址是:https://www.boost.org/,翻译这篇博文时Boost库的最新版本是1.73.0

线程同步

尽管使用多个线程可以提高应用程序的性能,但通常也增加了复杂性。 如果多个函数同时执行,则必须同步访问共享资源。 一旦应用程序达到一定大小,这将涉及大量的编程工作。 本节介绍Boost.Thread提供的用于同步线程的类。

Example 44.7 使用boost::mutex互斥访问

#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

boost::mutex mutex;

void thread()
{
using boost::this_thread::get_id;
for (int i = 0; i < 5; ++i)
{
  wait(1);
  mutex.lock();
  std::cout << "Thread " << get_id() << ": " << i << std::endl;
  mutex.unlock();
}
}

int main()
{
boost::thread t1{thread};
boost::thread t2{thread};
t1.join();
t2.join();
}

多线程程序使用互斥锁进行同步。 Boost.Thread提供了不同的互斥锁类,其中boost::mutex是最简单的。 互斥锁的基本原理是防止特定线程拥有互斥锁时其他线程获得所有权。 一旦被释放,其他线程即可获得所有权。 这将导致线程等待,直到拥有互斥锁的线程完成处理并释放其对该互斥锁的所有权为止。
示例44.7使用类型为boost::mutex的全局互斥锁,称为互斥锁。 thread()函数通过调用lock()获得此对象的所有权。 这是在函数写入标准输出流之前完成的。 写入消息后,将通过调用unlock()释放所有权。
main()创建两个线程,两个线程都在执行thread()函数。 每个线程计数到5,并在for循环的每次迭代中将一条消息写入标准输出流。 由于std::cout是线程共享的全局对象,因此访问必须同步。 否则,消息可能会混淆。 同步保证在任何给定时间只有一个线程可以访问std::cout。 两个线程都尝试在写入标准输出流之前获取互斥锁,但是实际上一次仅一个线程访问std::cout。 无论哪个线程成功调用lock(),所有其他线程都需要等待,直到调用unlock()。

获取和释放互斥锁是一种典型的方案,并且Boost.Thread通过不同类型支持它。 例如,可以使用boost::lock_guard而不是使用lock()和unlock()。

Example 44.8. 具有互斥释放保证的boost :: lock_guard

#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

boost::mutex mutex;

void thread()
{
  using boost::this_thread::get_id;
  for (int i = 0; i < 5; ++i)
  {
    wait(1);
    boost::lock_guard<boost::mutex> lock{mutex};
    std::cout << "Thread " << get_id() << ": " << i << std::endl;
  }
}

int main()
{
  boost::thread t1{thread};
  boost::thread t2{thread};
  t1.join();
  t2.join();
}

boost::lock_guard分别自动在其构造函数和其析构函数中调用lock()和unlock()。 例44.8中同步了对共享资源的访问,就像显式调用两个成员函数时一样。 类boost::lock_guard是RAII惯用语的一个示例,可确保在不再需要资源时将其释放。

除了boost::mutex和boost::lock_guard之外,Boost.Thread还提供其他类来支持同步变体。 其中之一是boost::unique_lock,它提供了一些有用的成员函数。

Example 44.9. 多功能锁 boost::unique_lock

#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

boost::timed_mutex mutex;

void thread1()
{
  using boost::this_thread::get_id;
  for (int i = 0; i < 5; ++i)
  {
    wait(1);
    boost::unique_lock<boost::timed_mutex> lock{mutex};
    std::cout << "Thread " << get_id() << ": " << i << std::endl;
    boost::timed_mutex *m = lock.release();
    m->unlock();
  }
}

void thread2()
{
  using boost::this_thread::get_id;
  for (int i = 0; i < 5; ++i)
  {
    wait(1);
    boost::unique_lock<boost::timed_mutex> lock{mutex,
      boost::try_to_lock};
    if (lock.owns_lock() || lock.try_lock_for(boost::chrono::seconds{1}))
    {
      std::cout << "Thread " << get_id() << ": " << i << std::endl;
    }
  }
}

int main()
{
  boost::thread t1{thread1};
  boost::thread t2{thread2};
  t1.join();
  t2.join();
}

示例44.9使用thread*(函数的两个变体。两种变体仍然在循环中向标准输出流写入五个数字,但是现在它们使用类boost::unique_lock来锁定互斥体。

thread1()将变量互斥锁传递给boost::unique_lock的构造函数,这使boost::unique_lock尝试锁定互斥锁。在这种情况下,boost::unique_lock的行为与boost::lock_guard相同。 boost::unique_lock的构造函数在互斥量上调用lock()。

但是,boost::unique_lock的析构函数不会在thread1()中释放互斥量。在thread1()中,在锁上调用release(),从而将互斥锁与锁解耦。默认情况下,boost::unique_lock的析构函数会释放互斥锁,就像boost::lock_guard的析构函数一样,但是如果互斥体解耦则不会。因此,在thread1()中明确调用了unlock()。

thread2()将互斥量和boost::try_to_lock传递给boost::unique_lock的构造函数。这使得boost::unique_lock的构造函数不调用互斥锁上的lock(),而是调用try_lock()。因此,构造函数仅尝试锁定互斥锁。如果互斥锁由另一个线程拥有,则尝试失败。

owns_lock()可让您检测boost::unique_lock是否能够锁定互斥锁。如果owns_lock()返回true,thread2()可以立即访问std::cout。如果owns_lock()返回false,则调用try_lock_for()。该成员函数还尝试锁定互斥锁,但是在失败之前,它会等待互斥锁指定的时间。在示例44.9中,锁尝试一秒钟以获得互斥量。如果try_lock_for()返回true,则可以访问std::cout。否则,thread2()放弃并跳过一个数字。因此,示例中的第二个线程可能不会在标准输出流中写入五个数字。

请注意,在示例44.9中,互斥锁的类型为boost::timed_mutex,而不是boost::mutex。该示例使用boost::timed_mutex,因为此互斥锁是唯一提供成员函数try_lock_for()的互斥锁。在锁上调用try_lock_for()时,将调用此成员函数。 boost::mutex仅提供成员函数lock()和try_lock()。

boost::unique_lock是排他锁。互斥锁始终是互斥锁的唯一所有者。互斥锁释放后,另一个锁才可以控制该互斥锁。 Boost.Thread还支持与boost::shared_lock类一起使用的共享锁,该类与shared_mutex一起使用。

Example 44.10. 使用boost::shared_lock的共享锁

#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>

void wait(int seconds)
{
  boost::this_thread::sleep_for(boost::chrono::seconds{seconds});
}

boost::shared_mutex mutex;
std::vector<int> random_numbers;

void fill()
{
  std::srand(static_cast<unsigned int>(std::time(0)));
  for (int i = 0; i < 3; ++i)
  {
    boost::unique_lock<boost::shared_mutex> lock{mutex};
    random_numbers.push_back(std::rand());
    lock.unlock();
    wait(1);
  }
}

void print()
{
  for (int i = 0; i < 3; ++i)
  {
    wait(1);
    boost::shared_lock<boost::shared_mutex> lock{mutex};
    std::cout << random_numbers.back() << '\n';
  }
}

int sum = 0;

void count()
{
  for (int i = 0; i < 3; ++i)
  {
    wait(1);
    boost::shared_lock<boost::shared_mutex> lock{mutex};
    sum += random_numbers.back();
  }
}

int main()
{
  boost::thread t1{fill}, t2{print}, t3{count};
  t1.join();
  t2.join();
  t3.join();
  std::cout << "Sum: " << sum << '\n';
}

如果线程仅需要对特定资源的只读访问权限,则可以使用boost::shared_lock类型的非排他锁。修改资源的线程需要写访问权限,因此需要排他锁。由于具有只读访问权限的线程不受同时读取相同资源的其他线程的影响,因此它可以使用非排他锁并共享互斥锁。

在示例44.10中,print()和count()都只读取变量random_numbers。 print()函数将random_numbers中的最后一个值写入标准输出流,而count()函数将其添加到变量sum中。因为这两个函数都不能修改random_numbers,所以两个函数都可以使用boost::shared_lock类型的非排他锁同时访问它。

在fill()函数内部,需要一个boost::unique_lock类型的排他锁,因为它将新的随机数插入random_numbers中。 fill()使用unlock()成员函数释放互斥量,然后等待一秒钟。与前面的示例不同,在for循环的末尾调用wait(),以确保在容器中至少有一个随机数被print()或count()访问之前。这两个函数在其for循环的开始都调用wait()函数。

从不同的位置查看对wait()函数的单个调用,一个潜在的问题变得显而易见:函数调用的顺序直接受CPU实际执行各个线程的顺序的影响。使用条件变量,可以同步各个线程,以便添加到random_numbers的值可以立即由其他线程处理。

Example 44.11. 带boost :: condition_variable_any的条件变量

#include <boost/thread.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>

boost::mutex mutex;
boost::condition_variable_any cond;
std::vector<int> random_numbers;

void fill()
{
  std::srand(static_cast<unsigned int>(std::time(0)));
  for (int i = 0; i < 3; ++i)
  {
    boost::unique_lock<boost::mutex> lock{mutex};
    random_numbers.push_back(std::rand());
    cond.notify_all();
    cond.wait(mutex);
  }
}

void print()
{
  std::size_t next_size = 1;
  for (int i = 0; i < 3; ++i)
  {
    boost::unique_lock<boost::mutex> lock{mutex};
    while (random_numbers.size() != next_size)
      cond.wait(mutex);
    std::cout << random_numbers.back() << '\n';
    ++next_size;
    cond.notify_all();
  }
}

int main()
{
  boost::thread t1{fill};
  boost::thread t2{print};
  t1.join();
  t2.join();
}

例44.11删除了wait()和count()函数。线程不再在每次迭代中等待一秒钟;相反,它们执行得尽可能快。另外,没有计算总数。数字只是写入标准输出流。

为了确保正确处理随机数,使用条件变量来同步各个线程,可以检查多个线程之间的某些条件。

和以前一样,fill()函数在每次迭代时都会生成一个随机数,并将其放置在random_numbers容器中。为了阻止其他线程同时访问该容器,使用了排他锁。本示例使用一个条件变量,而不是等待一秒钟。调用notify_all()将使用wait()唤醒一直在等待此通知的每个线程。

查看print()函数的for循环,您可以看到针对同一条件变量调用了成员函数wait()。当通过调用notify_all()唤醒线程时,它将尝试获取互斥量,只有在fill()函数中成功释放了互斥量之后,该互斥量才会成功。

这里的窍门是,调用wait()还会释放作为参数传递的互斥量。调用notify_all()之后,fill()函数通过调用wait()释放互斥量。然后,它将阻塞并等待其他线程调用notify_all(),一旦将随机数写入标准输出流,该事件就会在print()函数中发生。

注意,对print()函数内部的wait()成员函数的调用实际上发生在单独的while循环内。这样做是为了处理以下情况:在第一次在print()中调用wait()成员函数之前,已经在容器中放置了一个随机数。通过将random_numbers中存储的元素数与预期的元素数进行比较,可以成功处理此方案,并将随机数写入标准输出流。

如果锁不是for循环中的本地锁,而是在外部作用域中实例化的,则示例44.11也适用。实际上,这样做更有意义,因为不需要在每次迭代中都销毁并重新创建锁。由于互斥锁始终与wait()一起释放,因此您无需在迭代结束时销毁锁。

猜你喜欢

转载自blog.csdn.net/ccf19881030/article/details/106036460