Linux·C++多线程基础知识

2023-10-29

目录

1. 多线程

1.1 多进程与多线程

1.2 多线程理解

1.3 创建线程

1.4 join与detach方式

(1)join举例

(2)detach举例

1.5 this_thread

2. mutex

2.1 lock与unlock

2.2 lock_guard

2.3 unique_lock

3. condition_variable

3.1 wait

3.2 wait_for


1. 多线程

传统的C++(C++11之前)中并没有引入线程这个概念,在C++11出来之前,如果我们想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthread.h>,或者windows下的<windows.h> 。

C++11提供了语言层面上的多线程,包含在头文件<thread>中。它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11 新标准中引入了5个头文件来支持多线程编程,如下图所示:

1.1 多进程与多线程

  • 多进程并发

使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但是这也造就了多进程并发的两个缺点:

在进程间的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。
由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

  • 多线程并发

多线程并发指的是在同一个进程中执行多个线程。

优点:

有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。

缺点:

由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。

                摘自《C++ 并发编程》

1.2 多线程理解

  • 单CPU内核的多个线程。

一个时间片运行一个线程的代码,并不是真正意义的并行计算。

  • 多个cpu或者多个内核

可以做到真正的并行计算。

1.3 创建线程

创建线程很简单,只需要把函数添加到线程当中即可。

  • 形式1:
std::thread myThread ( thread_fun);//函数形式为void thread_fun()
myThread.join();
//同一个函数可以代码复用,创建多个线程
  • 形式2:
std::thread myThread ( thread_fun(100));
myThread.join();
//函数形式为void thread_fun(int x)
//同一个函数可以代码复用,创建多个线程
  • 形式3:
std::thread (thread_fun,1).detach();//直接创建线程,没有名字
//函数形式为void thread_fun(int x)
  • 代码举例

使用g++编译下列代码的方式:g++ test.cc -o test -l pthread

#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    cout<<"子线程1"<<endl;
}
void thread_2(int x)
{
    cout<<"x:"<<x<<endl;
  cout<<"子线程2"<<endl;
}
int main()
{
  thread first ( thread_1);     // 开启线程,调用:thread_1()
  thread second (thread_2,100);  // 开启线程,调用:thread_2(100)
  //thread third(thread_2,3);//开启第3个线程,共享thread_2函数。
  std::cout << "主线程\n";

  first.join(); //必须说明添加线程的方式            
  second.join(); 
  std::cout << "子线程结束.\n";//必须join完成
  return 0;
}

1.4 join与detach方式

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。比如上例中的join。

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
  • join方式,等待启动的线程完成,才会继续往下执行。

可以使用joinable判断是join模式还是detach模式。

if (myThread.joinable()) foo.join();

(1)join举例

下列代码中,join后面的代码不会被执行,除非子线程结束。

#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    while(1)
    {
        //cout<<"子线程1111"<<endl;
    }
}
void thread_2(int x)
{
    while(1)
    {
        //cout<<"子线程2222"<<endl;
    }
}
int main()
{
    thread first ( thread_1);     // 开启线程,调用:thread_1()
    thread second (thread_2,100);  // 开启线程,调用:thread_2(100)

    first.join();                // pauses until first finishes 这个操作完了之后才能destroyed
    second.join();               // pauses until second finishes//join完了之后,才能往下执行。
    while(1)
    {
        std::cout << "主线程\n";
    }
    return 0;
}

(2)detach举例

下列代码中,主线程不会等待子线程结束。如果主线程运行结束,程序则结束。

#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
    while(1)
    {
        cout<<"子线程1111"<<endl;
    }
}
void thread_2(int x)
{
    while(1)
    {
        cout<<"子线程2222"<<endl;
    }
}
int main()
{
    thread first ( thread_1);     // 开启线程,调用:thread_1()
    thread second (thread_2,100);  // 开启线程,调用:thread_2(100)

    first.detach();                
    second.detach();            
    for(int i = 0; i < 10; i++)
    {
        std::cout << "主线程\n";
    }
    return 0;
}

1.5 this_thread

this_thread是一个类,它有4个功能函数,具体如下:

using std::chrono::system_clock;
std::time_t tt = system_clock::to_time_t(system_clock::now());

struct std::tm * ptm = std::localtime(&tt);
cout << "Waiting for the next minute to begin...\n";
++ptm->tm_min; //加一分钟
ptm->tm_sec = 0; //秒数设置为0
//暂停执行,到下一整分执行
this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));

2. mutex

mutex头文件主要声明了与互斥量(mutex)相关的类。mutex提供了4种互斥类型,如下表所示。

 std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。

2.1 lock与unlock

mutex常用操作:

  • lock():资源上锁
  • unlock():解锁资源
  • trylock():查看是否上锁,它有下列3种类情况:

(1)未上锁返回false,并锁住;
(2)其他线程已经上锁,返回true;
(3)同一个线程已经对它上锁,将会产生死锁。

死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

下面结合实例对lock和unlock进行说明。

同一个mutex变量上锁之后,一个时间段内,只允许一个线程访问它。例如:
 

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx.lock();
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
  mtx.unlock();
}

int main ()
{
  std::thread th1 (print_block,50,'*');//线程1:打印*
  std::thread th2 (print_block,50,'$');//线程2:打印$

  th1.join();
  th2.join();

  return 0;
}

如果是不同mutex变量,因为不涉及到同一资源的竞争,所以下列代码运行可能会出现交替打印的情况,或者另一个线程可以修改共同的全局变量!!!

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

std::mutex mtx_1;           // mutex for critical section
std::mutex mtx_2;           // mutex for critical section

int test_num = 1;

void print_block_1 (int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx_1.lock();
  for (int i=0; i<n; ++i) {
      //std::cout << c;
      test_num = 1;
      std::cout<<test_num<<std::endl;
  }
  std::cout << '\n';
  mtx_1.unlock();
}
void print_block_2 (int n, char c) {
  // critical section (exclusive access to std::cout signaled by locking mtx):
  mtx_2.lock();
  test_num = 2;
  for (int i=0; i<n; ++i) {
      //std::cout << c;
      test_num = 2;
      std::cout<<test_num<<std::endl;
  }
  mtx_2.unlock();
}

int main ()
{
  std::thread th1 (print_block_1,10000,'*');
  std::thread th2 (print_block_2,10000,'$');

  th1.join();
  th2.join();

  return 0;
}

2.2 lock_guard

创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

lock_guard的特点:

  • 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
  • 不能中途解锁,必须等作用域结束才解锁
  • 不能复制

代码举例

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

int g_i = 0;
std::mutex g_i_mutex;  // protects g_i,用来保护g_i

void safe_increment()
{
    const std::lock_guard<std::mutex> lock(g_i_mutex);
    ++g_i;
    std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
    // g_i_mutex自动解锁
}

int main()
{
	std::cout << "main id: " <<std::this_thread::get_id()<<std::endl;
    std::cout << "main: " << g_i << '\n';

    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

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

    std::cout << "main: " << g_i << '\n';
}

说明:

  • 该程序的功能为,每经过一个线程,g_i 加1。
  • 因为涉及到共同资源g_i ,所以需要一个共同mutex:g_i_mutex。
  • main线程的id为1,所以下次的线程id依次加1。

2.3 unique_lock

简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。

unique_lock的特点:

  • 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
  • 可以随时加锁解锁
  • 作用域规则同 lock_grard,析构时自动释放锁
  • 不可复制,可移动
  • 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)

所有 lock_guard 能够做到的事情,都可以使用 unique_lock 做到,反之则不然。那么何时使lock_guard呢?很简单,需要使用锁的时候,首先考虑使用 lock_guard,因为lock_guard是最简单的锁。

下面是代码举例:
 

#include <mutex>
#include <thread>
#include <iostream>
struct Box {
    explicit Box(int num) : num_things{num} {}

    int num_things;
    std::mutex m;
};

void transfer(Box &from, Box &to, int num)
{
    // defer_lock表示暂时unlock,默认自动加锁
    std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);

    //两个同时加锁
    std::lock(lock1, lock2);//或者使用lock1.lock()

    from.num_things -= num;
    to.num_things += num;
    //作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}

int main()
{
    Box acc1(100);
    Box acc2(50);

    std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
    std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);

    t1.join();
    t2.join();
    std::cout << "acc1 num_things: " << acc1.num_things << std::endl;
    std::cout << "acc2 num_things: " << acc2.num_things << std::endl;
}

说明:

该函数的作用是,从一个结构体中的变量减去一个num,加载到另一个结构体的变量中去。

  • std::mutex m;在结构体中,mutex不是共享的。但是只需要一把锁也能锁住,因为引用传递后,同一把锁传给了两个函数。
  • cout需要在join后面进行,要不然cout的结果不一定是最终算出来的结果。
  • std::ref 用于包装按引用传递的值。
  • std::cref 用于包装按const引用传递的值。

3. condition_variable

condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。下面以condition_variable为例进行介绍。

condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。

enum class cv_status { no_timeout, timeout };

3.1 wait

当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。

在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。

代码示例:
 

#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;
bool shipment_available() {return cargo!=0;}

void consume (int n) {
    for (int i=0; i<n; ++i) {
        std::unique_lock<std::mutex> lck(mtx);//自动上锁
        //第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
        cv.wait(lck,shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo=0;
    }
}

int main ()
{
    std::thread consumer_thread (consume,10);

    for (int i=0; i<10; ++i) {
        //每次cargo每次为0才运行。
        while (shipment_available()) std::this_thread::yield();
        std::unique_lock<std::mutex> lck(mtx);
        cargo = i+1;
        cv.notify_one();
    }

    consumer_thread.join();,
    return 0;
}

说明:

  • 主线程中的while,每次在cargo=0才运行。
  • 每次cargo被置为0,会通知子线程unblock(非阻塞),也就是子线程可以继续往下执行。
  • 子线程中cargo被置为0后,wait又一次启动等待。也就是说shipment_available为false,则等待。

3.2 wait_for

与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。 而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。

template <class Rep, class Period>
  cv_status wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time);

另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。

template <class Rep, class Period, class Predicate>
       bool wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time, Predicate pred);

代码示例:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <chrono>             // std::chrono::seconds
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status

std::condition_variable cv;

int value;

void read_value() {
  std::cin >> value;
  cv.notify_one();
}

int main ()
{
  std::cout << "Please, enter an integer (I'll be printing dots): \n";
  std::thread th (read_value);

  std::mutex mtx;
  std::unique_lock<std::mutex> lck(mtx);
  while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
    std::cout << '.' << std::endl;
  }
  std::cout << "You entered: " << value << '\n';

  th.join();

  return 0;
}
  1. 通知或者超时都会解锁,所以主线程会一直打印。
  2. 示例中只要过去一秒,就会不断的打印。


原文链接:https://blog.csdn.net/QLeelq/article/details/115747717

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux·C++多线程基础知识 的相关文章

随机推荐

  • ARM编译Qt程序报错pinyin.cpp:1: error: stray ‘\357‘ in program

    ARM编译Qt程序报错pinyin cpp 1 error stray 357 in program 问题 Qt程序用gcc编译可以通过 用arm linux gcc编译文件的时候出现异常错误 pinyin cpp 1 error stra
  • 西瓜书学习笔记day2

    模型评估与选择 一 经验误差与过拟合 错误率 分类错误的样本数占样本总数的比例 在m个样本中共有a个样本分类错误 则错误率E a m 精度 1 a m 误差 学习器实际预测输入与样本的真实输出之间的差异定义为误差 在训练集中的误差被称为训练
  • 简单写一个随机在一个三维区域生成球的matlab函数

    1条消息 使用MATLAB在给定矩形区域随机绘制任意个不相交的圆 风一样的航哥的博客 CSDN博客 以上博客自从发表以来好几个同学都问我 三维的怎么实现 今天下班之后 就写一个 不知道是不是其他同学需要的 先上代码 function plo
  • Qt学习: QCloseEvent关闭事件的使用及代码示例

    QCloseEvent事件是指 当你鼠标点击窗口右上角的关闭按钮时 所触发的函数 如果你没有重写virtual closeEvent QCloseEvent event 这个虚函数的话 系统是默认接受关闭事件的 所以就会关闭窗体 但有的时候
  • ImportError: libcudart.so.10.0:cannot open shared object file: No such file or direct【mmdetection错误】

    问题 在使用mmdetection做训练的时候 出现错误 gt gt gt from mmdet apis import init detector Traceback most recent call last return bootst
  • 【IDEA】idea设置默认maven配置, 避免每次设置maven

    环境 IDEA 2018 2021 场景 每次导入新项目是 经常需要重新设置maven 非常麻烦 方案 idea设置默认maven配置 避免每次设置maven 方法 Step 1 打开Settings File gt Other Setti
  • cartographer 参数理解

    参考文章 cartographer参数调整 xjEzekiel 博客园 cartographer探秘第一章之安装编译与参数配置 李太白lx的博客 CSDN博客 cartographer 涉及到的参数需要增加删除或者修改尽量在velodyne
  • mysql connector net 5.0_mysql 数据库和net 的版本动态库搭配问题

    Connector NET 1 0 includes support for MySQL Server 4 0 4 1 and 5 0 features and full compatibility with the ADO NET dri
  • python实现弹球小游戏

    跟着趣味开发python一起实现的弹球小游戏 游戏运行效果 实现流程 1 创建游戏画布 创建ball类 2 增加几个动作 让小球移动 让小球来回反弹 改变小球的起始方向 3 加上球拍 使球拍左右移动 循环移动 4 增加输赢因素 对小球位置进
  • 运动补偿 & 运动估计

    运动补偿是一种描述相邻帧 相邻在这里表示在编码关系上相邻 在播放顺序上两帧未必相邻 差别的方法 具体来说是描述前面一帧 相邻在这里表示在编码关系上的前面 在播放顺序上未必在当前帧前面 的每个小块怎样移动到当前帧中的某个位置去 这种方法经常被
  • Effective Modern C++ Item 20 对于类似std::shared_ptr但有可能悬空的指针,使用std::weak_ptr

    如果需要某种智能指针能够像std shared ptr一样方便 但又无需参与管理所指涉到的对象的共享所有权的话 就很好适合用std weak ptr 但这样的功能同样会带来一个问题 这种指针需要处理一个对std shared ptr而言不是
  • softmax分类器_Softmax 理解

    Softmax深入理解 译 AIUAI www aiuai cn Pytorch的交叉熵nn CrossEntropyLoss在训练阶段 里面是内置了softmax操作的 因此只需要喂入原始的数据结果即可 不需要在之前再添加softmax层
  • OpenWrt-SDK-编译生成ipk软件包

    版本 Barrier Breaker 类型 brcm2708 下载SDK http downloads openwrt org barrier breaker 14 07 brcm2708 generic OpenWrt SDK brcm2
  • linux部署vue项目

    命令行进入配置文件 vi usr local nginx conf nginx conf 输入i进行修改端口号和文件路径 按ESC保存后输入 wq退出 进入sbin启动nginx cd usr local nginx sbin nginx
  • FMC164-基于JESD204B的4路1Gsps AD 4路1.25Gsps DA FMC子卡

    板卡介绍 FMC164子卡集成4通道1Gsps采样率 16位AD 4通道1 25Gsps 16位DA 板载时钟芯片HMC7044 可以提供JESD204B所需要的各种时钟 具有同步 触发功能 模拟信号采用SSMC射频连接器输入和输出 板卡可
  • es 修改mappings字段结构

    es不支持直接修改mappings字段结构 可以通过 reindex 重建索引 方式修改 POST reindex source index old index dest index new index op type create Ela
  • 记录:Qt Creator 10配置安卓开发环境

    Qt Creator 现在的安卓开发环境配置相比老版本方便了不少 本文以目前在线安装版的 Qt Creator 10 0 2 Qt 5 15 Qt 6 5 为例做演示 有些文件可能会因为网络问题需要科学上网才能下载 1 下载 JDK htt
  • 【css】css动画实现的3种方式

    css实现动画主要有3种方式 transition实现过渡动画 transform转变动画 animation实现自定义动画 一 transition过渡动画 1 语法 transition property duration timing
  • UnityAPI.Transform变换(Yanlz+Unity+API+Transform+)

    UnityAPI Transform变换 版本 作者 参与者 完成日期 备注 UnityAPI Transform V01 1 0 严立钻 2018 08 21 UnityAPI Transform变换 发布说明 UnityAPI Tran
  • Linux·C++多线程基础知识

    目录 1 多线程 1 1 多进程与多线程 1 2 多线程理解 1 3 创建线程 1 4 join与detach方式 1 join举例 2 detach举例 1 5 this thread 2 mutex 2 1 lock与unlock 2