C++17中的shared_mutex与C++14的shared_timed_mutex

2023-11-21 17:22:48 浏览数 (2)

1. 背景

在多线程的应用开发中,我们经常会面临多个线程访问同一个资源的情况,我们使用mutex(互斥量)进行该共享资源的保护,通过mutex实现共享资源的独占性,即同一时刻只有一个线程可以去访问该资源,前面我们介绍了C 11中使用互斥量互斥量的管理来避免多个读线程同时访问同一资源而导致数据竞争问题(即数据的一致性被遭到破坏)的发生,这里的数据竞争问题往往只涉及到多个线程写另外一个或多个线程读操作的时候,而对于多个线程进行读且不涉及写操作时,不存在数据竞争的问题。面对多线程涉及多访问,少读取的场景,我们有以下读写的例子:

示例1:

代码语言:javascript复制
#include <iostream>
#include <thread>
#include <mutex>
#include <time.h>

int value = 0;
std::mutex mutex;

// 将value的值复制给v,对value进行读取操作
void readValue(int &v)
{
  mutex.lock();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  v = value;
  mutex.unlock();
}

// 设置value的值为v,对value进行写入操作
void setValue(int v)
{
  mutex.lock();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  value = v;
  mutex.unlock();
}

int main()
{
  int read1;
  int read2;
  int read3;

  auto start = std::chrono::system_clock::now();
  std::thread t1(readValue, std::ref(read1));
  std::thread t2(readValue, std::ref(read2));
  std::thread t3(readValue, std::ref(read3));
  std::thread t4(setValue, 1);

  t1.join();
  t2.join();
  t3.join();
  t4.join();
  auto end = std::chrono::system_clock::now();

  std::cout << read1 << "n";
  std::cout << read2 << "n";
  std::cout << read3 << "n";
  std::cout << value << "n";
  auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
  std::cout << "runtime : " << elapsed.count() << "us" << 'n';
}

输出:

代码语言:javascript复制
0
0
0
1
runtime : 4017340us

从示例1的输出结果可以看出,对于value值的读取和写入都会获取其所有权,当一个线程获取其所有权,不管它是写入还是读取,都会导致其他线程阻塞,最终导致示例1的总的运行时间与每个线程依次单独执行的时长和类似,针对该应用场景,有没有更高效率的方法呢?

当在一个频繁读取共享数据,但只偶尔涉及写操作的场景时,我们希望存在一种在同一时刻可以允许多个线程进行读的操作,在需要写的时候再进行所有权的独占性的互斥量,于是C 提供了shared_timed_mutexshared_mutex两种共享互斥量。

:实际上shared_timed_mutex的原来的类名为shared_mutex,由于其带有定时功能,但是名字中未加入计时,违背了 timed_mutex recursive_timed_mutex 设定的命名先例,于是在 2014 年的 Issaquah ISO C 会议上,shared_mutex 根据 N3891 提案被重命名为 shared_timed_mutex,并为不定时shared_mutex留出空间,而且不带定时的shared_mutex这在某些平台上可能比shared_timed_mutex更有效。在C 17又提供了shared_mutex。两者的基本功能和用法类似,shared_mutex只是在shared_timed_mutex基础上删除了超时的功能。

2. shared_mutex

shared_mutex 类是C 17开始提供的一个同步原语,可用于保护共享数据不被多个线程同时访问。其在头文件<share_mutex>中定义,与便于独占访问的其他互斥类型不同,shared_mutex 拥有二个访问级别:

  • 共享 --- 多个线程能共享同一互斥的所有权。其对应的就是读的访问权限。
  • 独占性 --- 仅一个线程能占有互斥。其对应的就是写的访问权限。

若一个线程已获取独占性锁(通过 locktry_lock),则无其他线程能获取该锁(包括共享的)。仅当任何线程均未获取独占性锁时,共享锁能被多个线程获取(通过 lock_shared try_lock_shared)。在一个线程内,同一时刻只能获取一个锁(共享独占性)。

shared_mutex提供了排他性锁定方法有:

函数

功能描述

备注

lock

锁定互斥,若互斥不可用则阻塞

公有成员函数

try_lock

尝试锁定互斥,若互斥不可用则返回

公有成员函数

unlock

解锁互斥

公有成员函数

:通常不直接调用 lock() 和unlock(),而是用 std::unique_lock 与 std::lock_guard管理排他性锁定。

shared_mutex提供了共享锁定方法有:

函数

功能描述

备注

lock_shared

为共享所有权锁定互斥,若互斥不可用则阻塞

公有成员函数

try_lock_shared

尝试为共享所有权锁定互斥,若互斥不可用则返回

公有成员函数

unlock_shared

解锁互斥(共享所有权)

公有成员函数

对于示例1的问题,我们使用shared_mutex优化后的代码如下:

示例2:

代码语言:javascript复制
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <time.h>

int value = 0;
std::shared_mutex mutex;

// 将value的值复制给v,对value进行读取操作

void readValue(int &v)
{
  mutex.lock_shared();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  v = value;
  mutex.unlock_shared();
}

// 设置value的值为v,对value进行写入操作
void setValue(int v)
{
  mutex.lock();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  value = v;
  mutex.unlock();
}

int main()
{
  int read1;
  int read2;
  int read3;

  auto start = std::chrono::system_clock::now();
  std::thread t1(readValue, std::ref(read1));
  std::thread t2(readValue, std::ref(read2));
  std::thread t3(readValue, std::ref(read3));
  std::thread t4(setValue, 1);

  t1.join();
  t2.join();
  t3.join();
  t4.join();
  auto end = std::chrono::system_clock::now();

  std::cout << read1 << "n";
  std::cout << read2 << "n";
  std::cout << read3 << "n";
  std::cout << value << "n";
  auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

  std::cout << "runtime : " << elapsed.count() << "us" << 'n';
}

输出:

代码语言:javascript复制
0
0
0
1
runtime : 2007973us

从示例2的输出可以看出,总耗时缩短到2007973微秒,由于线程t1t2t3可以同时访问数据,大大提高了运行效率。

3. shared_timed_mutex

shared_timed_mutex 类拥有shared_mutex一样特性:拥有二个层次的访问:

  • 共享 --- 多个线程能共享同一互斥的所有权。其对应的就是读的访问权限。
  • 独占性 --- 仅一个线程能占有互斥。其对应的就是写的访问权限。

但与shared_mutex不同的是,其增加了与以类似 timed_mutex 的行为, shared_timed_mutex 提供通过 try_lock_for()try_lock_until()try_lock_shared_for()try_lock_shared_until() 方法,试图带时限地要求 shared_timed_mutex 所有权的能力。

shared_timed_mutex提供了排他性锁定方法有:

函数

功能描述

备注

lock

锁定互斥,若互斥不可用则阻塞

公有成员函数

try_lock

尝试锁定互斥,若互斥不可用则返回

公有成员函数

try_lock_for

尝试锁定互斥,若互斥在指定的时限时期中不可用则返回

公有成员函数

try_lock_until

尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回

公有成员函数

unlock

解锁互斥

公有成员函数

:通常不直接调用 lock()unlock(),而是用 std::unique_lock 与 std::lock_guard管理排他性锁定。

shared_timed_mutex提供了共享锁定方法有:

函数

功能描述

备注

lock_shared

为共享所有权锁定互斥,若互斥不可用则阻塞

公有成员函数

try_lock_shared

尝试为共享所有权锁定互斥,若互斥不可用则返回

公有成员函数

try_lock_shared_for

尝试为共享所有权锁定互斥,若互斥在指定的时限时期中不可用则返回

公有成员函数

try_lock_shared_until

尝试为共享所有权锁定互斥,若直至抵达指定时间点互斥不可用则返回

公有成员函数

unlock_shared

解锁互斥(共享所有权)

公有成员函数

示例3:

代码语言:javascript复制
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <time.h>

int value = 0;
std::shared_timed_mutex mutex;

// 将value的值复制给v,对value进行读取操作

void readValue(int &v)
{
  mutex.lock_shared();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  v = value;
  mutex.unlock_shared();
}

// 设置value的值为v,对value进行写入操作
void setValue(int v)
{
  mutex.lock();
  // 模拟一些延迟
  std::this_thread::sleep_for(std::chrono::seconds(1));
  value = v;
  mutex.unlock();
}

int main()
{
  int read1;
  int read2;
  int read3;

  auto start = std::chrono::system_clock::now();
  std::thread t1(readValue, std::ref(read1));
  std::thread t2(readValue, std::ref(read2));
  std::thread t3(readValue, std::ref(read3));
  std::thread t4(setValue, 1);

  t1.join();
  t2.join();
  t3.join();
  t4.join();
  auto end = std::chrono::system_clock::now();

  std::cout << read1 << "n";
  std::cout << read2 << "n";
  std::cout << read3 << "n";
  std::cout << value << "n";
  auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

  std::cout << "runtime : " << elapsed.count() << "us" << 'n';
}

输出:

代码语言:javascript复制
0
0
0
1
runtime : 2010207us

从示例3的输出结果来看,shared_timed_mutexshared_mutex的执行时间基本一致,shared_mutex略好于shared_timed_mutex,这正是由于其结构相较于shared_timed_mutex简单导致的。

4. 总结

shared_timed_mutexshared_mutex是一种具有共享和独占性的互斥量,其将读取和写入等不同的场景赋予不同的权限:

  • 共享访问
  • 独占访问

大大提高了多线程对共享资源仅读取访问时候的效率,如果不涉及计时相关的需求,share_mutex会是更好的选择。但是对于写读比较大的场景,其优势就发挥不出来,此时使用最简单的mutex效果会更好。

0 人点赞