首页 > 其他分享 >线程同步(互斥锁条件变量)

线程同步(互斥锁条件变量)

时间:2024-10-27 17:16:45浏览次数:3  
标签:加锁 lock 互斥 同步 线程 mutex wait

 线程同步

  • 互斥锁(互斥量)
  • 条件变量
  • 生产/消费者模型

一、互斥锁

C++11提供了四种互斥锁:

  • mutex:互斥锁。
  • timed_mutex:带超时机制的互斥锁。
  • recursive_mutex:递归互斥锁。
  • recursive_timed_mutex:带超时机制的递归互斥锁。

包含头文件:#include <mutex>

1、mutex类

1)加锁lock()

互斥锁有锁定和未锁定两种状态。

如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。

如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。

2)解锁unlock()

只有持有锁的线程才能解锁。

lock和unlock至少满足95%的应用场景!

3)尝试加锁try_lock()

如果互斥锁是未锁定状态,则加锁成功,函数返回true。

如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)

2、timed_mutex类

增加了两个成员函数:

bool try_lock_for(时间长度);

bool try_lock_until(时间点);

3、recursive_mutex类

递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。

4、lock_guard类

lock_guard是模板类,可以简化互斥锁的使用,也更安全。

lock_guard的定义如下:

template<class Mutex>
class lock_guard
{
  explicit lock_guard(Mutex& mtx);
}

lock_guard在构造函数中加锁,在析构函数中解锁。

lock_guard采用了RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。

二、条件变量-生产消费者模型

条件变量

  • 当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
  • 为了保护共享资源,条件变量需要和互斥锁结合一起使用
  • 生产/消费者模型(高速缓存队列)

255fcf5dd16f4cce8e2a84a585deb1a7.png

条件变量是一种线程同步机制。当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

C++11的条件变量提供了两个类:

condition_variable:只支持与普通mutex搭配,效率更高。

condition_variable_any:是一种通用的条件变量,可以与任意mutex搭配(包括用户自定义的锁类型)。

包含头文件:<condition_variable>

1、condition_variable类

主要成员函数:

1)condition_variable() 默认构造函数。

2)condition_variable(const condition_variable &)=delete 禁止拷贝。

3)condition_variable& condition_variable::operator=(const condition_variable &)=delete 禁止赋值。

4)notify_one() 通知一个等待的线程。

5)notify_all() 通知全部等待的线程。

6)wait(unique_lock<mutex> lock) 阻塞当前线程,直到通知到达。

7)wait(unique_lock<mutex> lock,Pred pred) 循环的阻塞当前线程,直到通知到达且谓词满足。

8)wait_for(unique_lock<mutex> lock,时间长度)

9)wait_for(unique_lock<mutex> lock,时间长度,Pred pred)

10)wait_until(unique_lock<mutex> lock,时间点)

11)wait_until(unique_lock<mutex> lock,时间点,Pred pred)

重点(wait(mutex)函数):

wait(mutex)做了三件事:

  1. 把互斥锁解锁。

  2. 阻塞,等待被唤醒。

  3. 被唤醒后,给互斥锁加锁。

2、unique_lock类

template <class Mutex> class unique_lock是模板类,模板参数为互斥锁类型。

unique_lock和lock_guard都是管理锁的辅助类,都是RAII风格(在构造时获得锁,在析构时释放锁)。它们的区别在于:为了配合condition_variable,unique_lock还有lock()和unlock()成员函数。

生产者消费者模型类示例:

#include <iostream>
#include <string>
#include <thread>                      // 线程类头文件。
#include <mutex>                      // 互斥锁类的头文件。
#include <deque>                      // deque容器的头文件。
#include <queue>                      // queue容器的头文件。
#include <condition_variable>  // 条件变量的头文件。
using namespace std;
class AA
{
    mutex m_mutex;                                    // 互斥锁。
    condition_variable m_cond;                  // 条件变量。
    queue<string, deque<string>> m_q;   // 缓存队列,底层容器用deque。
public:
    void incache(int num)     // 生产数据,num指定数据的个数。
    {
        lock_guard<mutex> lock(m_mutex);   // 申请加锁。
        for (int ii=0 ; ii<num ; ii++)
        {
            static int bh = 1;           // 超女编号。
            string message = to_string(bh++) + "号超女";    // 拼接出一个数据。
            m_q.push(message);     // 把生产出来的数据入队。
        }
        //m_cond.notify_one();     // 唤醒一个被当前条件变量阻塞的线程。
        m_cond.notify_all();          // 唤醒全部被当前条件变量阻塞的线程。
    }
    
    void outcache()   {    // 消费者线程任务函数。
        while (true)   {
            // 把互斥锁转换成unique_lock<mutex>,并申请加锁。
            unique_lock<mutex> lock(m_mutex);

            // 条件变量虚假唤醒:消费者线程被唤醒后,缓存队列中没有数据。
            //while (m_q.empty())    // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
            //    m_cond.wait(lock);  // 1)把互斥锁解开;2)阻塞,等待被唤醒;3)给互斥锁加锁。
            m_cond.wait(lock, [this] { return !m_q.empty(); });

            // 数据元素出队。
            string message = m_q.front();  m_q.pop();
            cout << "线程:" << this_thread::get_id() << "," << message << endl;
            lock.unlock();      // 手工解锁。

            // 处理出队的数据(把数据消费掉)。
            this_thread::sleep_for(chrono::milliseconds(1));   // 假设处理数据需要1毫秒。
        }
    }
};

int main()
{
    AA aa;
  
    thread t1(&AA::outcache, &aa);     // 创建消费者线程t1。
    thread t2(&AA::outcache, &aa);     // 创建消费者线程t2。
    thread t3(&AA::outcache, &aa);     // 创建消费者线程t3。

    this_thread::sleep_for(chrono::seconds(2));    // 休眠2秒。
    aa.incache(2);      // 生产2个数据。

    this_thread::sleep_for(chrono::seconds(3));    // 休眠3秒。
    aa.incache(5);      // 生产5个数据。

    t1.join();   // 回收子线程的资源。
    t2.join();
    t3.join(); 
}

流程:

  • 程序运行,因为wait把互斥锁解开了,所以三个消费者都能加锁成功,现在wait到了第二步,三个线程都被阻塞在条件变量的wait()函数中,此时互斥锁没有被任何线程占有;
  • 生产者往队列中放完数据后,会发出条件信号;
  • wait()函数接收到i先弄好之后,不一定立即返回,他还要申请加锁,加锁成功后才会返回;
  • 如果wait()返回了,一定申请到了锁,接下来可以让队列中的数据出队,出对后再解锁。

消费者线程中的while(m_q.empty())循环:

条件变量存在虚假唤醒的情况:消费者线程被唤醒后,缓存队列中没有数据(三个消费者线程,一次生产两个数据,然后notifyall,全部唤醒,肯定有一个线程拿不到数据,被虚假唤醒了)。如果被虚假唤醒了应该继续等待下一次通知,所以用if肯定不行,必须用while。

也可以用wait(unique_lock<mutex> lock,Pred pred) 版本,添加一个谓词:

m_cond.wait(lock, [this] { return !m_q.empty(); });

效果是一样的,本质上这个重载的wait函数中也有个while循环。

 

 

标签:加锁,lock,互斥,同步,线程,mutex,wait
From: https://blog.csdn.net/weixin_56520780/article/details/143265387

相关文章

  • MySQL中如何实现数据的增量更新和同步
    ​​在MySQL数据库操作中,实现数据的增量更新和同步至关重要。主要步骤包括:1.确定增量数据源与目标库;2.使用binlog进行增量数据跟踪;3.利用工具进行数据同步;4.确保同步过程的安全性;5.定期监控与验证同步准确性。首先,我们必须识别哪些数据需要进行增量更新。1.确定增量数据源与目......
  • 如果并发1000个请求url,通过虚拟线程应该怎么处理
    在Java中,如果要通过虚拟线程(VirtualThreads)处理1000个并发请求,能够有效提升吞吐量,同时避免传统线程池模型的线程资源开销。虚拟线程是JDK19引入的ProjectLoom的一部分,在JDK21中正式成为LTS版的稳定特性。下面是一个使用虚拟线程并发1000个请求的示例代码,并解释它的工作原理。......
  • 2024年最新互联网大厂精选 Java 面试真题集锦(JVM、多线程、MQ、MyBatis、MySQL、Redis
    前言春招,秋招,社招,我们Java程序员的面试之路,是挺难的,过了HR,还得被技术面,在去各个厂面试的时候,经常是通宵睡不着觉,头发都脱了一大把,还好最终侥幸能够入职一个独角兽公司,安稳从事喜欢的工作至今...近期也算是抽取出大部分休息的时间,为大家准备了一份通往大厂面试的小捷径,准备......
  • 虚拟线程一般应用在哪些场景
    虚拟线程(VirtualThreads)是Java21引入正式发布的特性之一,它极大简化了并发编程,特别适合高并发应用场景。与传统的操作系统线程相比,虚拟线程轻量级且数量可以大幅增加,因此在某些业务需求中具有优势。以下是常见的虚拟线程应用场景:1.高并发的IO密集型应用虚拟线程非常适合处理......
  • 虚拟线程实现Web服务器(处理HTTP请求)
    使用Java虚拟线程开发一个Web服务器可以显著提升并发能力,同时保持代码简洁。在这个示例中,我们将使用Java21的虚拟线程实现一个简单的HTTPWeb服务器,逐步解释每个部分的代码。实现目标通过虚拟线程处理HTTP请求。创建一个简单的Web服务器来响应客户端请求。适配IO......
  • Spring Boot 应用使用虚拟线程来并行处理REST请求
    在Java中,虚拟线程(VirtualThreads)是JDK19引入的预览功能,并在JDK21中正式发布。SpringBoot从3.2版本开始支持虚拟线程,可以有效提升并发性能。下面我详细讲解如何在SpringBoot应用中使用虚拟线程并行处理REST请求。一、虚拟线程简介传统Java应用使用操作系统......
  • 虚拟线程一般应用在哪些场景
    虚拟线程(VirtualThreads)是Java21引入正式发布的特性之一,它极大简化了并发编程,特别适合高并发应用场景。与传统的操作系统线程相比,虚拟线程轻量级且数量可以大幅增加,因此在某些业务需求中具有优势。以下是常见的虚拟线程应用场景:1.高并发的IO密集型应用虚拟线程非常适合处理......
  • C#线程详解及应用示例
     简介在编写应用程序实现业务功能过程中,为解决吞吐量和响应效率的问题,我们会用到多线程、异步编程两项重要的技术。通过它们来提高应用程序响应和高效。应用程序每次运行都会启动一个进程(进程是一种正在执行的程序),而进程中可以包含一个或多个线程,由应用程序入口直接或间接执......
  • 节能型后台数据同步器:HarmonyOS Next的智能延迟任务管理
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。一、项目背景与需求分析后台数据同步......
  • 轻易云平台成功实现金蝶云星空与马帮的数据同步
    新增供应商同步金蝶=》马帮(测试通过)在企业信息化系统中,数据的高效集成与同步是确保业务流程顺畅运行的关键环节。本文将分享一个实际案例,展示如何通过轻易云数据集成平台实现金蝶云星空与马帮之间的数据对接。具体方案为“新增供应商同步金蝶=》马帮(测试通过)”。在这个案例中,我......