深入理解C++多线程中的数据共享与保护(c++ concurrency in action 第三章总结)

深入理解C++多线程中的数据共享与保护(c++ concurrency in action 第三章总结)

在现代C++中,多线程编程已经成为提高程序性能的关键技术之一。然而,与此同时,处理多线程程序中的共享数据也带来了许多新的挑战。如何确保数据在多线程环境下的安全访问,以及如何避免常见的多线程问题如死锁和竞争条件,是每个C++开发者必须面对的问题。本文将结合《c++ concurrency in action》一书的第三章内容,深入探讨这些问题并给出相应的解决方案。

1. 共享数据的问题与竞争条件

在多线程编程中,共享数据的安全性是一个至关重要的问题。当多个线程需要同时访问或修改同一数据时,往往会引发竞争条件(Race Condition)。这就像两个孩子同时想要玩一个玩具,如果没有明确的使用规则,他们很可能会因为争抢玩具而发生冲突。
举个例子,假设我们有一个简单的链表,如果一个线程正在删除链表中的某个节点,而另一个线程恰好在遍历链表,那么第二个线程很可能会遇到链表结构不完整的情况,导致程序崩溃或数据错误。这种情况下,链表的完整性在删除操作过程中被破坏,从而引发了竞争条件。

为了避免这种问题,我们通常需要使用**互斥锁(std::mutex)**来保护共享数据,确保同一时刻只有一个线程能够访问或修改这些数据。然而,使用锁虽然能解决数据一致性问题,却可能引发另一个棘手的问题——死锁。

#include <iostream>
#include <list>
#include <mutex>
#include <thread>

std::mutex mut;
std::list<int> lis;

void add_to_list(int value) {
    std::lock_guard<std::mutex> guard(mut);  // 使用lock_guard自动管理锁的开闭
    lis.push_back(value);
}

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

int main() {
    std::thread t1([] { add_to_list(1); });
    std::thread t2([] { add_to_list(2); });
    std::thread t3([] { std::cout << std::boolalpha << list_contains(1) << std::endl; });

    t1.join();
    t2.join();
    t3.join();

    for (auto const& ele : lis) {
        std::cout << ele << std::endl;
    }

    return 0;
}

2. 死锁与如何避免

死锁(Deadlock)通常发生在两个或多个线程彼此等待对方释放资源的情况下,导致所有线程都无法继续执行。一个典型的例子是,两个线程分别锁住了两个互斥锁,并且都在等待对方释放锁。由于双方都不释放锁,结果导致程序陷入僵局。

为了避免死锁,C++标准库提供了std::lock函数。这个函数可以一次性锁定多个互斥锁,避免线程在锁定多个资源时发生死锁。此外,C++17引入了std::scoped_lock,它可以更方便地处理多个锁的同时锁定。

在实际应用中,我们还可以通过以下几种方式避免死锁:

  • 避免嵌套锁定:尽量避免在已经持有一个锁的情况下再去锁定另一个锁。如果必须这么做,可以使用std::lock来确保锁定的顺序一致。
  • 避免在持有锁时调用用户代码:调用外部代码时,无法控制其行为,可能导致其试图锁定其他资源,从而引发死锁。
  • 锁定顺序一致:在多个线程中锁定资源时,确保所有线程以相同的顺序锁定资源。

下面的代码展示如何按一定的层次顺序锁定,value大的锁先于value小的上锁,解锁时相反

#include <mutex>
#include <stdexcept>
#include <thread>
class hierarchical_mutex {
 public:
  explicit hierarchical_mutex(unsigned long value) : hierarchy_value(value), previous_hierarchy_value(0) {}
  void lock() {
    check_for_hierarchy_violation();
    internal_mutex.lock();
    update_hierarchy_value();
  }
  void unlock() {
    if (this_thread_hierarchy_value != hierarchy_value) {
      throw std::logic_error("mutex hierarchy violated");
    }
    this_thread_hierarchy_value = previous_hierarchy_value;
    internal_mutex.unlock();
  }
  bool try_lock() {
    check_for_hierarchy_violation();
    if (!internal_mutex.try_lock()) {
      return false;
    }
    update_hierarchy_value();
    return true;
  }

 private:
  std::mutex internal_mutex;
  unsigned long const hierarchy_value;
  unsigned long previous_hierarchy_value;
  static thread_local unsigned long this_thread_hierarchy_value;
  void check_for_hierarchy_violation() const {
    if (this_thread_hierarchy_value <= hierarchy_value) {
      throw std::logic_error("mutex hierarchical violated");
    }
  }
  void update_hierarchy_value() {
    previous_hierarchy_value    = this_thread_hierarchy_value;
    this_thread_hierarchy_value = hierarchy_value;
  }
};
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
hierarchical_mutex other_mutex(6000);
int do_low_level_stuff();

int low_level_func() {
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex);
  return do_low_level_stuff();
}

void high_level_stuff(int some_param);

void high_level_func() {
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex);
  high_level_stuff(low_level_func());
}

void thread_a() {
  high_level_func();
}

void do_other_stuff();

void other_stuff() {
  high_level_func();
  do_other_stuff();
}

void thread_b() {
  std::lock_guard<hierarchical_mutex> lk(other_mutex);  
  //错误的,含有value为6000锁的线程不能锁上value为10000的
  other_stuff();
}

3. 递归锁std::recursive_mutex

有些情况下,同一个线程可能需要多次锁定同一个互斥锁。此时,如果使用普通的std::mutex,将会导致死锁。std::recursive_mutex允许线程多次锁定同一个互斥锁,直至显式释放所有的锁。

#include <iostream>
#include <mutex>
#include <thread>

std::recursive_mutex rec_mutex;

void recursive_function(int count) {
    if (count < 1) return;
    std::lock_guard<std::recursive_mutex> guard(rec_mutex);
    std::cout << "Recursion level: " << count << std::endl;
    recursive_function(--count);
}

int main() {
    std::thread t1(recursive_function, 5);
    t1.join();
    return 0;
}

}

4. 线程安全的栈:threadsafe_stack

设计线程安全的数据结构时,使用std::mutex和std::lock_guard的组合非常常见。下面是一个线程安全栈的实现,它通过std::mutex来保护内部数据结构,防止竞态条件的发生。

#include <exception>
#include <memory>
#include <mutex>
#include <stack>

struct empty_stack : std::exception {
    const char* what() const throw() {
        return "Empty stack!";
    }
};

template<typename T>
class threadsafe_stack {
private:
    std::stack<T> data;
    mutable std::mutex m;

public:
    threadsafe_stack() = default;
    threadsafe_stack(const threadsafe_stack& other) {
        std::lock_guard<std::mutex> lock(other.m);
        data = other.data;
    }

    void push(T new_value) {
        std::lock_guard<std::mutex> lock(m);
        data.push(std::move(new_value));
    }

    std::shared_ptr<T> pop() {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty()) throw empty_stack();
        std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
        data.pop();
        return res;
    }

    bool empty() const {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
    }
};

5. 使用std::unique_lock进行灵活锁定

std::unique_lock提供了比std::lock_guard更灵活的锁定方式。它可以延迟锁定、提前解锁,并支持锁的所有权转移。

#include <mutex>
#include <thread>

std::mutex m;

void process_data() {
    std::unique_lock<std::mutex> lk(m);
    // 处理数据
    lk.unlock();  // 提前解锁以提高并发性
    // 进一步处理无需保护的数据
    lk.lock();  // 重新加锁
}

int main() {
    std::thread t1(process_data);
    std::thread t2(process_data);

    t1.join();
    t2.join();

    return 0;
}

6.锁的粒度:如何选择适当的锁定范围

在多线程编程中,锁的粒度是指锁定的资源范围以及锁定的持续时间。选择合适的锁粒度是编写高效并发程序的关键。锁的粒度太粗会导致不必要的线程阻塞,降低并发性;而锁的粒度太细则可能增加死锁的风险,或因频繁的锁操作带来性能开销。因此,平衡锁的粒度非常重要。

锁的粒度和代码示例
以下是一个使用std::unique_lock来管理锁粒度的示例:

#include <mutex>
#include <thread>

std::mutex m;

void process_data() {
    std::unique_lock<std::mutex> lk(m);
    // 处理需要保护的数据
    // 由于锁住了整个互斥量,其他线程会在此处阻塞,直到解锁。
    lk.unlock();  // 提前解锁以提高并发性

    // 进一步处理无需保护的数据
    // 此处操作不需要持有锁,因此可以并发执行,从而提高程序的效率。

    lk.lock();  // 重新加锁以处理需要保护的数据
    // 再次操作受保护的数据
}

int main() {
    std::thread t1(process_data);
    std::thread t2(process_data);

    t1.join();
    t2.join();

    return 0;
}

锁的粒度讨论

在上面的代码中,process_data函数展示了如何通过调整锁的持有时间来控制锁的粒度。该函数执行了以下几个步骤:

  • 获取锁并处理受保护的数据:开始时,函数锁定了互斥量m,并对受保护的数据进行了处理。这是典型的锁定模式,用于防止多个线程同时修改共享资源。

  • 提前解锁以提高并发性:完成受保护数据的操作后,函数立即解锁。这一解锁操作使得其他线程能够尽早获得锁,从而提高了整个程序的并发性能。

  • 处理无需保护的数据:在解锁状态下,函数继续处理那些不需要保护的数据。由于这些操作不依赖于共享资源,因此可以并发执行。

  • 重新加锁以处理更多受保护的数据:在需要再次访问共享资源时,函数重新加锁。这确保了对共享数据的访问始终是安全的。

通过这样做,我们将锁的粒度控制在一个合理的范围内:即只在真正需要时才持有锁。这种策略可以减少不必要的线程阻塞,提高程序的并发性和性能。

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值