共享互斥锁和互斥锁的区别(为什么C++17中两者都存在)?

14

5
你应该查看文档而不是头文件摘要(就像你提供的链接所示)。顺便说一句,那是C++17而不是C++11。最接近的是C++14,即 std::shared_timed_mutex。不过,你听说过“读写锁”吗? - WhiZTiM
3个回答

34

使用普通的互斥锁,可以保证对于某种关键资源的独占访问 - 除此之外没有其他访问。共享互斥锁通过允许两种级别的访问(共享和独占)来扩展此功能,如下所示:

  • 独占访问防止任何其他线程获取互斥锁,就像使用普通互斥锁一样。无论其他线程尝试获取共享访问还是独占访问都不重要。
  • 共享访问允许多个线程获取互斥锁,但所有它们只能以共享模式获取。除非所有先前的共享持有者已经返回了互斥锁(通常,在独占请求等待期间,新的共享请求排队等待独占访问后被授予),否则将不授予独占访问。

一个典型的情况是数据库:如果几个线程同时读取相同的数据,则并不重要。但是修改数据库是关键的-如果某个线程读取数据而另一个线程正在写入,则可能会收到不一致的数据。因此,在允许写入之前,必须完成所有读取,并且必须等待写入完成后才能进行新的读取。写入后,进一步的读取可以再次同时发生。

编辑:旁注:

为什么读者需要锁?

这是为了防止在读取尚未完成时,写者获取该锁。此外,如果独占持有该锁,则会防止新读取程序获取该锁。


2
非常清楚,我们不希望读者在修改正在进行时阅读过时的数据,反之亦然,因此我们不希望编写者开始进行更改以保持数据一致性。 - Anand Kulkarni

5
一个共享互斥量有两级访问,“共享”和“独占”。 多个线程可以获得共享访问权限,但只有一个线程可以持有“独占”的访问权限(这包括没有共享访问权限)。
常见的情况是读/写锁。请注意,数据竞争只会在两个线程访问相同数据时发生,其中至少有一个是写入。
优点是数据可以被多个读者读取,但当作者需要访问时,他们必须获得对数据的独占访问权限。
为什么两个都要呢?一方面,独占锁构成了一个普通的互斥锁,因此可能只需要共享锁。但是,在共享锁实现中可能存在开销,可以通过使用功能较少的类型来避免这些开销。
下面是一个示例(略微改编自此处的示例http://en.cppreference.com/w/cpp/thread/shared_mutex)。
#include <iostream>
#include <mutex>  
#include <shared_mutex>
#include <thread>
  
std::mutex cout_mutex;//Not really part of the example...
void log(const std::string& msg){
    std::lock_guard guard(cout_mutex);
    std::cout << msg << std::endl;
}
 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
 
  // Multiple threads/readers can read the counter's value at the same time.
  unsigned int get() const {
    std::shared_lock lock(mutex_);//NB: std::shared_lock will shared_lock() the mutex.
    log("get()-begin");
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    auto result=value_;
    log("get()-end");
    return result;
  }
 
  // Only one thread/writer can increment/write the counter's value.
  void increment() {
    std::unique_lock lock(mutex_);
    value_++;
  }
 
  // Only one thread/writer can reset/write the counter's value.
  void reset() {
    std::unique_lock lock(mutex_);
    value_ = 0;
  }
 
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
 
int main() {
  ThreadSafeCounter counter;
 
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      auto ctr=counter.get();
      {
          std::lock_guard guard(cout_mutex);
          std::cout << std::this_thread::get_id() << ' ' << ctr << '\n';
      }
    }
  };
 
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
  std::thread thread3(increment_and_print);
 
  thread1.join();
  thread2.join();
  thread3.join();
}

可能的部分输出:
get()-begin
get()-begin
get()-end
140361363867392 2
get()-end
140361372260096 2
get()-begin
get()-end
140361355474688 3
//Etc...

请注意,两个get()-begin()返回值表明两个线程在读取期间持有共享锁。

4

我知道,但是线程可以同时读取数据,对吧?为什么读者需要锁定呢?只有修改需要锁定,以确保底层数据结构不会被破坏。 - Anand Kulkarni
1
@AnandKulkarni 读者需要锁定,否则他们可能在另一个线程正在写入相同数据时读取数据。这是为了防止数据竞争。使用读/写锁,多个读者可以同时访问数据。 - 0xBADF00

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