首页 > 编程语言 >C++ 中的线程、锁和条件变量

C++ 中的线程、锁和条件变量

时间:2024-11-18 13:56:01浏览次数:1  
标签:std 变量 lock C++ 线程 using mutex include

C++ 中的线程、锁和条件变量

  • Created: 2024-06-19T17:17+08:00
  • Published: 2024-11-18T10:39+08:00
  • Categories: C-CPP

目录

#include <thread>
#include <mutex>
#include <condition_variable>
using std::thread;
using std::mutex;
using std::condition_variable;

C++多线程并发基础入门教程 - 知乎

线程创建与执行

  1. 线程的参数就是函数名和函数的参数

  2. 线程创建就立刻运行

  3. main 如果要等待线程执行结束,需要调用 thread.join(),并且只能调用一次,多次调用 join 会报一个莫名其妙的错:

    terminate called after throwing an instance of 'std::system_error'
    what():  Invalid argument
    

    不 join 的话也会报错:

    terminate called without an active exception
    
  4. std::this_thread::get_id() 获取 thread_id

下面是在 main() 中专门启动一个 thread 打印 vector<int> 的例子:

#include <thread>
#include <iostream>
#include <vector>

using std::vector;
using std::mutex;

void log_vec(const vector<int> &vec)
{
    std::cout << "thread_id: " << std::this_thread::get_id() << std::endl;
    for (auto &x : vec)
    {
        std::cout << x << std::endl;
    }
    return;
}


int main()
{
    vector<int> v{1, 2, 3};
    thread t{log_vec, v};
    t.join();
    return 0;
}

#include<mutex>

  1. mutex:最简单的互斥锁,mutual exclusion,需要手动调用 lock()unlock() 上锁解锁
    一个线程内只能 lock() 一次,第二次 lock() 会被卡住。
    但是 unlock() 可以调用多次

  2. guard_lock<mutex>:为了防止忘记对 mutex 解锁,guard_lock 析构的时候会解锁。
    是一种 RAII
    构造时候自动上锁,也可以通过 adopt_lock 不自动上锁。
    注意:使用 std::lock_guard 后不能手动 lock()与手动 unlock()
    下面是它的定义:

     /** @brief A simple scoped lock type.
    *
    * A lock_guard controls mutex ownership within a scope, releasing
    * ownership in the destructor.
    */
     template<typename _Mutex>
     class lock_guard
     {
     public:
       typedef _Mutex mutex_type;
    
       explicit lock_guard(mutex_type& __m) : _M_device(__m)
       { _M_device.lock(); }
    
       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;
     };
    
  3. unique_lock: 在 guard_lock 的基础上,可以手动 lock 和 unlock,且支持 move

lock guard example

利用 lock_guard 来管理锁,没有 lock_guard.unlock(),直接利用 scope 解锁。

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

using std::mutex;
using std::thread;

mutex m;
int cnt = 0;
int max = 10;

void count(int tid)
{
    while (true)
    {
        { // use this bracket pair to create a scope
            std::lock_guard<mutex> lg(m);
            if (cnt >= max)
            {
                return;
            }
            cnt += 1;
            std::cout << "thread " << tid << " count " << cnt << std::endl;
        } // out of scope, lock_guard unlock
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    return;
}

int main()
{

    std::thread t1{count, 1};
    std::thread t2{count, 2};
    t1.join();
    t2.join();
    return 0;
}

mutex 底层实现解释 by GPT

mutex 的底层实现也有区别,在现代操作系统上,如果线程尝试 lock 一个已经被 lock 的 mutex,并不会因为 spinlock 而占用很多的 CPU 资源。

现代操作系统通常提供了多种底层机制来实现互斥锁(mutex)。以下是一些常见的底层实现方式:

Test-and-Set(测试并设置):这是一种基于硬件指令的实现方式。它使用原子操作来检查互斥锁的状态并设置锁的状态。如果锁已被占用,则线程会进入忙等待状态,不断尝试获取锁。这种实现方式在单处理器系统上效果较好,但在多处理器系统上可能会导致性能问题。

Compare-and-Swap(比较并交换):这也是一种基于硬件指令的实现方式。它使用原子操作来比较锁的当前状态和期望状态,并在两者相等时交换锁的状态。如果锁已被占用,则线程会进入忙等待状态,不断尝试获取锁。这种实现方式在多处理器系统上效果较好,因为它可以避免一些性能问题。

Semaphore(信号量):信号量是一种计数器,用于控制对共享资源的访问。互斥锁可以使用二进制信号量实现,其中计数器的值为 0 或 1。当线程尝试获取锁时,如果计数器为 0,则线程会被阻塞,直到计数器变为 1。当线程释放锁时,计数器会减少并唤醒等待的线程。

Futex(快速用户空间互斥锁):Futex 是一种用户空间的互斥锁实现方式。它利用了操作系统提供的原子操作和等待唤醒机制。当线程尝试获取锁时,如果锁已被占用,则线程会进入休眠状态,将自己添加到等待队列中。当锁被释放时,操作系统会唤醒等待的线程。

这些底层实现方式可能因操作系统和硬件平台的不同而有所差异。现代操作系统通常会根据具体的场景和需求选择适当的实现方式来提供高效的互斥锁机制。

条件变量(condition variable)

条件变量允许线程「等待」和「被唤醒」。

比如生产者消费者模型中,消费者在拿到锁后检查队列发现没有东西需要消费,那么就需要释放锁,并且进入等待状态,
直到有生产者告诉消费者有东西可以消费了,消费者再启动。

如果没有条件变量,可以用轮询实现:

// consumer use spin to query queue is free or not

while (true):
    lock queue
    if (queue.size() == 0):
        unlock queue
    else:
        elem = queue.pop()
        unlock queue
        consume elem

当队列为空的时候,会有一堆消费者一直在一个 while 里面抢锁,轮询待消费队列使否有东西,导致 CPU 浪费。
如果没有东西可以消费,我们希望所有消费者都 sleep,直到有人 wakeup 他。
消费者:

// consumer can wait

while (true):
    lock queue
    while (queue.size() == 0):
        unlock & sleep
        wakeup & get lock

	// 此时可以保证既 lock queue & queue.size() != 0
	elem = queue.pop()
    unlock queue
    consume elem

生产者:

while(true):
    lock queue
    if queue is full:
        unlock queue
        notify all consumers
    else:
        add 1 element to queue
        unlock queue and notify 1 consumer

condition_variable 的作用就是这个。

  • condition_variable.wait(unique_lock): wait 会释放 lock 并让线程进入 sleep 状态。
    线程被唤醒后,会退出 sleep 状态,并一直尝试获得 lock,直到获得 lock 才继续执行。
    要保证在 wait 前 mutex 已经被 lock。
  • condition_variable.notify_all(): 唤醒所有在 cv 上 sleep 的线程,它们都会去抢锁。
    所以要保证在此之前已经释放了 lock,不然被唤醒的线程都会去尝试获得 lock,但是没释放导致无法得到 lock。

线程从 cv.wait(lock) 被唤醒后会自动抢锁

如果一堆线程都卡在 wait(lock) 上,进行一次 notify_all(), 会唤醒所有线程去争夺 lock,
如果没有争夺到,也会脱离 wait 的状态,阻塞在 lock.lock() 上。

以下是验证代码,通过一次 notify_all 让所有线程都醒过来并获取 lock,见 ./wai-notify.cpp

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>
#include <vector>
#include <condition_variable>

using std::condition_variable;
using std::lock_guard;
using std::mutex;
using std::thread;
using std::unique_lock;
using std::vector;

condition_variable cv;
int num_thread = 3;
mutex m;

void wakeup_resume() {
    unique_lock<mutex> ql(m);
    cv.wait(ql);
    std::cout << "resume" << std::endl;
}

int main()
{
    thread threads[num_thread];
    for (int i = 0; i < num_thread; ++i) {
        threads[i] = thread{wakeup_resume};
    }

    std::this_thread::sleep_for(std::chrono::seconds(1)); // let all thread block in wait(), or cv.notify_all() run too fast so no thread in wait
    cv.notify_all();

    for (int i = 0; i < num_thread; ++i) {
        threads[i].join();
    }

    return 0;
}

虚假唤醒

虚假唤醒相关资料:

简而言之, cv.wait(lock) 之后的线程不一定被 cv.notify() 唤醒,所以醒来以后,原来要求 wait 的条件可能改变,所以 cv.wait 在一个 while 循环里面。

为了支持「事件」机制,所以使用了条件变量。
为了避免虚假唤醒,所以要写成 while。

生产者消费者代码例子

下面提供一份代码,假设一个生产者,一堆消费者对一个 queue 操作。
因为要处理的任务是有限的,任务没了以后使用 no_more_job 标记没有任务了,让 consumer 退出。

代码见 ./producer-consumer.cpp

#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>
#include <vector>
#include <queue>
#include <condition_variable>
#include <atomic>
using std::atomic;
using std::condition_variable;
using std::lock_guard;
using std::mutex;
using std::queue;
using std::thread;
using std::unique_lock;
using std::vector;

mutex m;
condition_variable cv;
queue<int> q{};
int q_cap = 3;

bool no_more_job = false;
int consumer_num = 3;
atomic<int> quit_consumer_num = 0;

void consumer()
{
    unique_lock<mutex> ul(m, std::defer_lock);
    while (true)
    {
        ul.lock();
        while (q.empty()) // use while to avoid "spurious wakeup"
        {
            cv.wait(ul); // wait will automatically release lock
            if (no_more_job & q.empty())
            {
                ul.unlock();
                quit_consumer_num.fetch_add(1);
                return;
            }
        }
        auto x = q.front();
        q.pop();
        std::cout << "consumer " << std::this_thread::get_id() << " get " << x << std::endl;
        ul.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

void producer()
{
    queue<int> to_add({1, 2, 3, 4, 5, 6, 7, 8, 9});

    while (!to_add.empty())
    {
        m.lock();
        if (q.size() < q_cap)
        {
            int x = to_add.front();
            to_add.pop();
            q.push(x);
            std::cout << "producer add " << x << std::endl;
            m.unlock();
            cv.notify_all();
        }
        else
        { // q is full
            m.unlock();
            cv.notify_all();
        }
    }
    no_more_job = true;
    while (quit_consumer_num != consumer_num)
    {
        cv.notify_one();
    }

    return;
}

int main()
{
    thread p{producer};
    thread threads[consumer_num];
    for (int i = 0; i < consumer_num; ++i)
    {
        threads[i] = std::thread{consumer};
    }

    for (int i = 0; i < consumer_num; ++i)
    {
        threads[i].join();
    }
    p.join();

    return 0;
}

-pthread-lpthread

-pthread-lpthread 有什么关系?

因为 thread 的实现有不同的版本,有的版本 .h 不兼容 Posix API,所以编译的过程中,需要指定用 Posix API 兼容的线程实现。在编译命令中,通过 -pthread 会通过加宏实现这个需求。
同时 -pthread 还指定链接实现了 Posix API 的 lib,所以不再需要 -lpthread

参考:

多线程 gdb debug

info threads 查看当前进程的线程
thread <ID>  切换调试的线程为指定 ID 的线程
break test.c:100 thread all   在所有线程中相应的行上设置断点

set scheduler-locking off|on
  off   默认值,不锁定任何线程,所有线程都执行
  on    只有当前被调试程序会执行

GDB 多线程之旅 - 知乎

试题

多线程数据存取

给了两个 api,一个取出数据,一个给入数据,需要实现一个中间函数让大量数据能够按顺序填入,限制单次填入的数据量。要求占用内存少,运行速度快。

该试题来源于网络,背景描述非常模糊。
解法是生产者和消费者操作 ring-buffer,并且消费者可以 sleep 不要轮询。

标签:std,变量,lock,C++,线程,using,mutex,include
From: https://www.cnblogs.com/dutrmp19/p/18552470

相关文章

  • C++二级:数字字符求和
    数字字符求和请编写一个程序实现以下功能:从一个字符串中,提取出所有的数字字符即0-9,并作为数求和。输入一行字符串,长度不超过100,字符串中不含空格。输出字符串中所有数字字符作为数的和样例输入Lsd2f02k3ja3sdf223样例输出171、数字字符求和请编写一个程序实现以下功......
  • C++刷题第十题——求奇数的乘积
    ProblemDescription给你n个整数,求他们中所有奇数的乘积。Input输入数据包含多个测试实例,每个测试实例占一行,每行的第一个数为n,表示本组数据一共有n个,接着是n个整数,你可以假设每组数据必定至少存在一个奇数。Output输出每组数中的所有奇数的乘积,对于测试实例,输出一行。Sa......
  • 《 C++ 修炼全景指南:二十 》不止是链表升级!跳表的核心原理与超强性能解析
    摘要这篇博客全面解析了跳表(SkipList)作为一种高效的链表数据结构的特性和应用。跳表以多层链表和随机化策略实现O(logn)的查找、插入和删除性能,简化了平衡树结构中常见的复杂旋转操作。通过剖析跳表的结构设计和核心操作,我们探讨了其在范围查询和动态更新中的优势,......
  • c++ 后端
    基础知识1.指针、引用2.数组3.缺省参数4.函数重载5.内联函数6.宏7.auto8.const9.类和对象10.类的6个默认成员函数11.初始化列表12.this指针13.C/C++的区别14.C++三大特性15.结构体内存对齐规则16.explicit17.static18.友元类、友元函数19.内部类20.......
  • C语言在linux上实现进程线程开发
    环境操作系统:Linux线程创建线程创建函数#include<pthread.h>/**@description线程创建函数*@paramtidp线程标识符*@paramattr线程属性指针*@paramstart_rtn线程执行函数(void*fun(void*))*@paramarg线......
  • wincc 7.5SP2下VBA编程学习练习15:批量删除变量
    在前面练习的基础上学习批量删除变量。新建下面的脚本:SubDeleteTags()'批量删除变量DimhmigoAshmigoDimstrTagNameAsStringDimiAsIntegerSethmigo=NewhmigoFori=1To5strTagName="Real"&CStr(i)hmigo.DeleteTagstrTagNameNextSethmigo=Nothin......
  • 读取和设置 Ubuntu 环境变量(很全面)
    读取和设置Ubuntu环境变量(很全面)环境变量在Ubuntu中,环境变量是一些动态命名值,用于影响程序的运行方式。它们包含了系统或用户的配置信息,帮助程序在运行时获取必要的环境信息。环境变量通常存储在内存中,程序可以通过这些变量访问到相关的数据。常用的环境变量......
  • C++ 编程基础(8)模版 | 8.2、函数模版
    文章目录一、函数模版1、声明与定义2、模版参数3、模板的实例化3.1、隐式实例化3.2、显示实例化4、模版的特化5、注意事项6、总结前言:C++函数模板是一种强大的特性,它允许程序员编写与类型无关的代码。通过使用模板,函数或类可以处理不同的数据类型,而无需重复编写......
  • 【C++】C++11 新特性揭秘:序章
    C++语法相关知识点可以通过点击以下链接进行学习一起加油!命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇类和对象-下篇日期类C/C++内存管理模板初阶String使用String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与QueuePriority......
  • 宝宝的C++,小学生C++编程启蒙 书籍等
    1、宝宝的C++(2016-11)2、啊哈编程星球:一本书入门Python和C++(2019年09月)啊哈编程星球啊哈编程星球!编程学习从这开始~3、我的第一本算法书(修订版)--2024.24、聪明的算法(2022.07)--6到12岁小读者量身打造的前沿科学大揭秘系列科普书5、走进GoC的编程世界(......