当前位置: 首页 > article >正文

高阶开发基础——快速入门C++并发编程3

目录

关于互斥量死锁

解决方案

顺序上锁。

超时机制

使用RAII来保证锁的恰当释放


关于互斥量死锁

上一篇博客中,我们看到了mutex可以保护数据,但是这也引来了新的麻烦,那就是互斥量的死锁。

互斥量(Mutex)是用于保护共享资源的一种同步机制。死锁(Deadlock)是指多个线程或进程因竞争资源而陷入相互等待的状态,导致程序无法继续执行。死锁通常发生在以下四个条件同时满足时:

  1. 互斥条件:资源一次只能被一个线程占用。

  2. 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源。

  3. 非抢占条件:线程已持有的资源不能被其他线程强行抢占,只能由线程主动释放。

  4. 循环等待条件:存在一个线程的循环链,每个线程都在等待下一个线程所持有的资源。

说的很空,我们现在来看一段代码:

#include <iostream>
#include <thread>
#include <mutex>
​
std::mutex mutex1;
std::mutex mutex2;
​
void thread1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    std::lock_guard<std::mutex> lock2(mutex2);
    std::cout << "Thread 1 finished" << std::endl;
}
​
void thread2() {
    std::lock_guard<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    std::lock_guard<std::mutex> lock1(mutex1);
    std::cout << "Thread 2 finished" << std::endl;
}
​
int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
​
    t1.join();
    t2.join();
​
    return 0;
}

上面的代码就是一段死锁代码。分析一下,thread1先启动了,快速的占用了mutex1,thread2随后占用了mutex1.麻烦来了,现在做好了一系列操作后,我们马上有需要获取第二个锁,对于thread1而言,他要拿到mutex2,就必须让thread2放掉他自己手中的mutex2,但是,thread2想要放掉自己的mutex2,就必须拿到thread1手中的mutex1...好!你一眼发现,这是双方都盯着对方的资源而不肯松手自己的资源导致的,现在程序卡死了。

解决方案

顺序上锁。

void thread1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    std::lock_guard<std::mutex> lock2(mutex2);
    std::cout << "Thread 1 finished" << std::endl;
}
​
void thread2() {
    std::lock_guard<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作
    std::lock_guard<std::mutex> lock1(mutex1);
    std::cout << "Thread 2 finished" << std::endl;
}

你看,这是资源锁获取顺序不一致导致的。调成一致,就可以有效的回避死锁(必须按照顺序请求资源),但麻烦之处在于,我们就丧失了一定的灵活性!所以,当资源的请求顺序是无所谓的时候,可以采用这个办法。但是,效率必然不会高!

另一个办法是——优化自己的编程思路,看看是不是真的不得不用多重锁,如果优化到后面,发现可以使用其他方案解决,那就更好了。

超时机制

还有一种,如果你的上锁是比较宽松的,允许获取锁失败,可以尝试使用超时集制:

std::timed_mutex mutex1;
std::timed_mutex mutex2;
​
void thread1() {
    auto start = std::chrono::steady_clock::now();
    while (true) {
        // 愿意花费500ms时间等待 
        if (mutex1.try_lock_for(std::chrono::milliseconds(100))) {
            // 愿意花费500ms时间等待 
            if (mutex2.try_lock_for(std::chrono::milliseconds(100))) {
                // works...
                std::cout << "Thread 1 finished" << std::endl;
                mutex2.unlock();
                mutex1.unlock();
                return;
            }
            mutex1.unlock();
        }
        // 如果超过1s没有拿到锁,放弃,退到后面我们处理
        if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) {
            std::cout << "Thread 1 timeout" << std::endl;
            return;
        }
    }
}

使用RAII来保证锁的恰当释放

哎呀!很不小心的,我们写下了这段代码:

static int shared_value;
std::mutex mtx;
void worker() {
    for (int i = 0; i < 1000000; i++) {
        mtx.lock();
        shared_value++;
    }
}
​
int main() {
    auto thread1 = std::thread(worker);
    auto thread2 = std::thread(worker);
    thread1.join();
    thread2.join();
​
    std::print("value of shared is: {}\n", shared_value);
}

你看到问题了嘛?答案是——我们的锁忘记解锁了,再下一轮循环中,请求一个永远不可能解锁的锁!因为锁了锁的人是过去的他自己!

麻烦了,这个倒还好,如果我们的逻辑非常复杂,如何保证我们自己还记得释放锁呢?答案是请出来万能的RAII机制。

 /** @brief A simple scoped lock type.
   *
   * A lock_guard controls mutex ownership within a scope, releasing
   * ownership in the destructor.
   *
   * @headerfile mutex
   * @since C++11
   */
  template<typename _Mutex>
    class lock_guard
    {
    public:
      typedef _Mutex mutex_type;
​
      [[__nodiscard__]]
      explicit lock_guard(mutex_type& __m) : _M_device(__m)
      { _M_device.lock(); }
​
      [[__nodiscard__]]
      lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m)
      { } // calling thread owns mutex
​
      ~lock_guard()
      { _M_device.unlock(); }
​
      lock_guard(const lock_guard&) = delete;
      lock_guard& operator=(const lock_guard&) = delete;
​
    private:
      mutex_type&  _M_device;
    };
​
  /// @} group mutexes
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace

看懂了嘛?这就是RAII思路设计的lock_guard。就是说,当我们构造了一个类lock_guard开始,我们就对他立马进行上锁,到出作用域,程序马上就要放手没法管住mutex的控制的时候,手动的进行释放处理操作,在这里,释放的操作就是将锁进行解锁(显然我们的锁必然已经被上锁了,因为锁在被lock_guard控制的一瞬间就调用了lock方法!)

但是还是不够灵活,我们如果想自己控制锁的范围,办法是——使用unique_ptr的姊妹类——unique_lock

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。


http://www.kler.cn/a/531026.html

相关文章:

  • 构建一个数据分析Agent:提升分析效率的实践
  • 【信息系统项目管理师-选择真题】2021上半年综合知识答案和详解
  • Leetcode 8283 移除排序链表中的重复元素
  • ESP32-c3实现获取土壤湿度(ADC模拟量)
  • java异常处理——try catch finally
  • MiniQMT与xtquant:量化交易的利器
  • 掌握 HTML5 多媒体标签:如何在所有浏览器中顺利嵌入视频与音频
  • va_list va_start va_end的概念和使用案例
  • python:如何播放 .spx 声音文件
  • Mac电脑上最新的好用邮件软件比较
  • Docker环境下Nacos的保姆级安装教程
  • UE PlayerState
  • 【FreeRTOS 教程 八】直达任务通知
  • YOLOV11-1:YoloV11-安装和CLI方式训练模型
  • 使用Express.js和SQLite3构建简单TODO应用的后端API
  • cf div3 998 E(并查集)
  • 几种用户鉴权的方式对比
  • Kamailio、MySQL、Redis、Gin后端、Vue.js前端等基于容器化部署
  • 讲清逻辑回归算法,剖析其作为广义线性模型的原因
  • volatile变量需要减少读取次数吗
  • 49【服务器介绍】
  • 常见的 Vue.js 组件库:Element Plus, Vuetify, Quasar
  • NeuralCF 模型:神经网络协同过滤模型
  • docker pull Error response from daemon问题
  • [HOT 100] 2824. 统计和小于目标的下标对数目
  • FreeRTOS从入门到精通 第十九章(内存管理)