第2章 线程间共享数据

1. 用互斥保护共享数据

  • 不变量(invariant):关于一个特定数据结构总为 true 的语句,比如 双向链表的两个相邻节点 A 和 B,A 的后指针一定指向 B,B 的前指针一定指向 A。有时程序为了方便会暂时破坏不变量,这通常发生于更新复杂数据结构的过程中,比如删除双向链表中的一个节点 N,要先让 N 的前一个节点指向 N 的后一个节点(不变量被破坏),再让 N 的后节点指向前节点,最后删除 N(此时不变量重新恢复)
  • 线程修改共享数据时,就会发生破坏不变量的情况,此时如果有其他线程访问,就可能导致不变量被永久性破坏,这就是 race condition
  • 如果线程执行顺序的先后对结果无影响,则为不需要关心的良性竞争。需要关心的是不变量被破坏时产生的 race condition
  • C++ 标准中定义了 data race 的概念,指代一种特定的 race condition,即并发修改单个对象。data race 会造成未定义行为
  • race condition 要求一个线程进行时,另一线程访问同一数据块,出现问题时很难复现,因此编程时需要使用大量复杂操作来避免 race condition
  • 在访问一个数据结构前,先锁住与数据相关的互斥;访问结束后,在解锁互斥。通过构造 std::mutex的实例来创建互斥,调用成员函数lock()加锁,
    调用unlock()解锁。C++标准库提供了类模板std::lock_guard,针对互斥实现了RAII手法:
    在构造时互斥加锁,在析构时解锁。
#include <list>
#include <mutex>
#include <algorithm>
#include <iostream>

std::list<int> some_list;
std::mutex some_mutex;

void add_to_list(int new_value) {
    std::lock_guard<std::mutex> guard(some_mutex);
    some_list.push_back(new_value);
}

bool list_contains(int value_to_find) {
    std::lock_guard<std::mutex> guard(some_mutex);
    return std::find(some_list.begin(),some_list.end(),value_to_find)
           != some_list.end();
}

int main() {
    add_to_list(42);
    std::cout << "contains(1)=" << list_contains(1)
              <<", contains(42)=" << list_contains(42) << std::endl;
}
  • C++17 提供了的 std::scoped_lock,它可以接受任意数量的 mutex,并将这些 mutex 传给 std::lock 来同时上锁,它会对其中一个 mutex 调用 lock(),对其他调用 try_lock(),若 try_lock() 返回 false 则对已经上锁的 mutex 调用 unlock(),然后重新进行下一轮上锁,标准未规定下一轮的上锁顺序,可能不一致,重复此过程直到所有 mutex 上锁,
    从而达到同时上锁的效果。C++17 支持类模板实参推断,可以省略模板参数。
#include <iostream>
#include <mutex>

class A {
public:
    [[maybe_unused]] static void lock() { std::cout << 1; }

    [[maybe_unused]] static void unlock() { std::cout << 2; }

    [[maybe_unused]] static bool try_lock() {
        std::cout << 3;
        return true;
    }
};

class B {
public:
    [[maybe_unused]] static void lock() { std::cout << 4; }

    [[maybe_unused]] static void unlock() { std::cout << 5; }

    [[maybe_unused]] static bool try_lock() {
        std::cout << 6;
        return true;
    }
};

int main() {
    A a;
    B b;
    {
        std::scoped_lock l(a, b);  // 16
        std::cout << std::endl;
    }  // 23
}
  • 一般 mutex 和要保护的数据一起放在类中,定义为 private 数据成员,而非全局变量,这样能让代码更清晰。但如果某个成员函数返回指向数据成员的指针或引用,则通过这个指针的访问行为不会被 mutex 限制,因此需要谨慎设置接口,确保 mutex 能锁住数据
#include <mutex>

class A {
public:
    void f() {}
};

class B {
public:
    A* get_data() {
        std::lock_guard<std::mutex> l(m_);
        return &data_;
    }

private:
    std::mutex m_;
    A data_;
};

int main() {
    B b;
    A* p = b.get_data();
    p->f();  // 未锁定 mutex 的情况下访问数据
}
posted @ 2022-06-30 23:04  coubusier  阅读(18)  评论(0)    收藏  举报