C++多线程初步

1.多线程初步

1.包含的库

#Include<thread>

2.涉及到的类 std::thread(这个类是属于标准模版库的,底层封装的系统调用)
3.代码实例

#include <iostream>
#include <thread>  
void hello(){    
    std::cout << "Hello World" << std::endl;
}
int main(){
    std::thread t(hello);
    // std::thread thread{ [] {std::cout << "Hello World!\n"; } };
    t.join();
    return 0;
}

线程函数含有参数的时候,将参数写在后面即可

void print(int x) { 
    std::cout << x;
}
int main() {
    int value = 42;
    thread t(print,value);
    t.join();
    return 0;
}

特殊例子,当参数传参方式为引用时

#include <iostream>
#include <thread>
#include <functional> 

void increment(int& x) { 
    x*=2;
}
int main() {
    int value = 42;
    //补全代码
   std::thread t(increment,std::ref(value));
    std::cout<<value; //期望是84
    return 0;
}

2.共享数据与线程同步

1.互斥量的介绍

当我们有多个线程任务对同一块共享数据进行读写时,为了协调这些线程对共享数据的访问,我们进入互斥量这一概念,在C++中,也就是std::mutex

当第一个线程执行到lock的时候,它拿到cpu的执行权,继续执行线程逻辑。当另外的线程执行到lock的时候,它会暂时陷入自旋,此时它会独占一个线程通道,自选期间如果lock的线程执行了unlock它就可以继续执行下去。但是如果lock的线程迟迟执行不到unlock,那么操作系统是不会允许这个线程通道一直被这个自旋的线程独占,这时候会从用户态转入内核态,让出线程通道来让别的线程执行。

还有一种不需要进入内核态的锁,是std::atomic ,原子变量的加速无需经过操作系统的处理,是通过编译器用原子指令实现的,所以原子量的效率优于线程锁。

2.关于粒度

锁本身不会造成严重的性能开销,但是加锁不当会导致程序转入内核态,这个过程有巨大的性能开销,所以我们在写C++代码的时候除了要注意共享数据的安全性,也要注意程序的性能,核心要点就是不要让一个线程陷入长时间的等待。

程序在哪些关键步骤需要加锁被称之为锁的粒度,如果程序上锁了无需上锁的语句,在逻辑和功能上看起来没有任何问题,但是却会导致性能上面的问题,所以上锁的时候我们首先应该关心锁的正确性,其次就是关心锁的粒度,既在一个多线程环境下,哪些步骤是必须需要加锁的,哪些步骤无需加锁。

3.lock_guard

  • lock_guard 是一个RAII(资源获取即初始化)风格的锁管理类,用于自动管理互斥锁(mutex)的加锁和解锁。
  • 它在构造时锁定互斥锁,在析构时自动释放锁,确保即使发生异常也不会忘记解锁。
    注意:lock_guard不支持手动lock()unlock()通常与{}结合来控制其生命期,进而控制临界区

4.thread_local

  • thread_local 是一个存储类说明符,用于声明线程局部变量
  • 每个线程都有其独立的变量副本,线程之间不会共享这些变量。
    注:访问thread_local的开销比普通变量大
    实例代码
#include<iostream>
#include<thread>
#include<mutex>

thread_local int tls_var=0;
std::mutex m;
void thread_func(int id){
     tls_var=id;
     std::lock_guard<std::mutex>lg(m);//锁的自动释放和上锁
     std::cout<<"thread id="<<std::this_thread::get_id()<<",  tls_var="<<tls_var<<std::endl;
}
void case05(){
     std::thread t1(thread_func,1);
     std::thread t2(thread_func,2);
     t1.join();
     t2.join();
}

5.unique_lock

  • unique_lock 是一个更灵活的锁管理类,与lock_guard类似,但提供了更多功能。
  • 它支持手动加锁解锁、延迟加锁、条件变量等高级功能。
    代码实例

6.关于死锁

两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。

例:比如我们有两个线程代表两台服务器,一个服务器用于数据查询存储,一台用于数据计算。然后我们日常生活中有两种任务,一种任务是先查询数据,然后使用查询到的数据进行计算。另一种是先计算数据,然后将计算结果保存。这时候如果我们仅仅对每台服务器单独加锁,就会出现一种场景,当两种任务同时到来的时候,任务A执行完成想要获取计算服务器,但是计算服务器被任务B占用;任务B想要获取查询存储服务器但是被任务A占用。

解决死锁总的来说有两种解法:一种是我们不对可能出现冲突的局部加锁,而是对整个任务加锁。就像上面的例子中,当任务A来的时候,同时对数据服务器和计算服务器加锁,计算完成之后释放。这样就不会出现死锁。
另一种解法是当一个任务持有锁超时之后就释放任务,短暂等待后重新执行任务。
问题:这两种解法分别适用于什么场景?

7.关于条件变量

日常生活中有一类常见的问题,那就是生产者消费者问题,这类型问题几乎无处不在。比如我们观看在线视频。这里有一个逻辑就是视频的数据来源于网络,数据的使用者是本地播放器。因此这里就有一个模型,即只有下载够一定数量的数据的时候播放器才开始播放,当下载数据超过一个限度的时候就停止缓存,当播放(使用数据)到一定程度的时候又继续下载。
这个模型里下载器扮演了生产者的角色,播放器扮演了消费者的角色。

实例代码:
情景
我们假设一个场景,有一个下载线程和一个播放线程,下载线程检测到当前没有任何数据就开始下载,直到下载数据数量达到5(我们假设一次下载任务执行可以下载一个数据),下载数量达到5之后下载线程停止工作,播放线程开始工作,直到消耗完下载线程的所有数据。请用C++代码表示这个过程。
我们假设上面的例子不是一次性的,而是有一个场景,在这个场景下我们这两个线程长期存在,线程A会一致下载文件到本地磁盘中,当线程A下载文件大小占满磁盘空间时候,我们就暂停下载并且开启B线程,B线程会解析处理这些文件,然后将其删除,当B线程删除文件到磁盘空间为0的时候,我们就暂停B线程并重新启动A线程。

std::mutex mut;
std::condition_variable cond_var;

int disk_capacity = 3;

std::mutex print_mutex;
void safe_print(const std::string& msg)
{
    std::lock_guard<std::mutex> guard(print_mutex);
    std::cout << msg << std::endl;
}

void download_file()
{
    std::unique_lock<std::mutex> lock(mut);

    while (true)
    {
        cond_var.wait(lock,[]{
        return disk_capacity ;
            });

        while (disk_capacity)
        {
            std::stringstream ss;
            ss << "files download ready, disk_capacity is " << disk_capacity;
            std::string formatted_string = ss.str();
            safe_print(formatted_string);
            std::this_thread::sleep_for(std::chrono::seconds(1));
            disk_capacity--;
        }
        cond_var.notify_all();
    }
}

void get_files_do_something()
{
    std::unique_lock<std::mutex> lock(mut);

    while (true)
    {
        cond_var.wait(lock, [] {
            return disk_capacity == 0;
            });
        //...解析文件,并且将磁盘清空
        disk_capacity = 3;
        std::stringstream ss;
        ss << "files delete, disk_capacity is " << disk_capacity << "-----" << std::this_thread::get_id();
        std::string formatted_string = ss.str();
        safe_print(formatted_string);
        cond_var.notify_all();
    }
}

int main()
{
    std::thread t1(get_files_do_something);
    std::thread t2(download_file);
    std::thread t3(get_files_do_something);
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

对于条件变量的补充:
1.几个重要方法

  • std::condition_variable::wait(...)如果谓词返回为true,则线程继续运行,返回为false,则线程阻塞,并且释放锁
  • std::condition_variable::notify_all() 唤醒其它所有线程,尝试拿锁,如果拿到锁,则做谓词条件判断,注意,此操作并不会使线程释放锁
    实例代码补充:
    第二版
#include<iostream>
#include<thread>
#include<mutex>
#include<queue>
#include<condition_variable>
/*
producer and consumer problem
*/
const int N=5;
std::queue<int>buffer;
std::mutex mtx;
std::condition_variable con_var;
void producer(){
    std::unique_lock<std::mutex>lock(mtx);
    while(1){
          con_var.wait(lock,[](){
          return buffer.size()==0;
      });
      for(int i=0;i<N;i++){
         buffer.push(i+1);
         std::cout<<"producer produce data:"<<i+1<<std::endl;
      }
      std::cout<<std::endl;
      std::this_thread::sleep_for(std::chrono::seconds(2)); 
      con_var.notify_all();
    }
}
void consumer(int id){
       while(1){
        {
        std::unique_lock<std::mutex>lock(mtx);
        con_var.wait(lock,[](){
            return !buffer.empty(); 
        });
        int data=buffer.front();
        buffer.pop();
        std::cout<<"consumer:"<<id<<" consume data:"<<data<<std::endl;
        if(buffer.empty()){
          std::cout<<std::endl;
          con_var.notify_all();
        } 
        }
        std::this_thread::sleep_for(std::chrono::seconds(2));//超级底层,要是不做细微延时的话,会出现独占锁的情况

    }
}
void case06(){
     std::thread t1(producer);
     std::thread t2(consumer,1);
     std::thread t3(consumer,2);
     std::thread t4(consumer,3);
     std::thread t5(consumer,5);
     t1.join();
     t2.join();
     t3.join();
     return;
}

实例代码补充结果如下:

可以看到,当数据队列(共享资源)为空时,生产者线程一次性写5个数据,唤醒所有消费者线程;当数据队列不为空时,5个消费者线程并发的消费数据,当数据被消耗殆尽时,唤醒所有线程(其实旨在让生产者拿到锁,进入活动)

3.轻量级的异步操作和原子操作

posted @ 2025-04-02 00:28  噫~该死的碳酸饮料  阅读(215)  评论(2)    收藏  举报