c++多线程

🏅Thread

c++11 引入标准库std::thread,来扩展c++对多线程方面的支持

参考文献一

参考文献二

参考文献三

基于c++实现的c++线程池实现参考

▶️构建函数

// ① 
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;

▶️共有函数

get_id

std::thread::id get_id() const noexcept;

类似的std::this_thread命名空间内也提供了get_id()函数用于获取当前线程的线程ID

join && detach && joinable

join()函数用于阻塞主线程,等待子线程执行完毕去除阻塞作用,主线程再继续执行,子线程销毁随着主线程销毁而销毁
detach()函数用于从当前主线程分离出去,不会阻塞主线程,当主线程执行完销毁时子线程也随之销毁。

#include <iostream>
#include <thread>
#include <chrono> // 时间函数库
void fun1(int sleep_for,const char* name)
{
  //当前线程沉睡sleep_for ms
  std::this_thread::sleep_for(std::chrono::milliseconds(sleep_for));
  std::cout<<name<<"线程ID::"<<std::this_thread::get_id()<<"\n";
}
void fun2()
{
  //当前线程沉睡100ms
  std::this_thread::sleep_for(std::chrono::milliseconds(100));
  std::cout<<"fun2"<<"线程ID::"<<std::this_thread::get_id()<<"\n";
}
int main()
{
  //主线程
  std::thread t1(fun1,200,"fun1"); // 调用构造3
  std::thread t2(fun2); // 调用构造2
  auto res1 = t1.joinable(); //true
  auto res2 = t2.joinable(); //true
  /*
  ** 线程对象创建后如果指定任务函数,则线程开始执行,主线程和子线程建立连接
  ** 此时jionable()函数返回为true,如果该子线程进行了join()操作,则在join()
  ** 语句之前都是joinable的
  */
  //join
  t1.join(); //程序在此阻塞直到子线程t1执行完毕
  auto res1 = t1.joinable(); //false
  t2.detach(); //在此不阻塞,但子线程t2会随着主线程销毁而销毁,主线程不会等待t2执行完成,因此t2可能会没执行完就被销毁
  auto res2 = t2.joinable(); //false
  return 0;
}

operator= && hardware_concurrency

std::thread 重载了=操作符,但由于线程之间不能进行复制,只能进行资源的转移,因此std::thread之间的资源转移需要借助移动语义进行实现,函数原型如下:

thread& operator=(thread&& other) noexcept; 
thread& operator=(thraed& other) = delete; //禁止复制拷贝

// thread t = std::move(t1) 资源转移从t1------>t

hardware_concurrency是std::thread提供的静态函数,用于查询cpu的核心数量

static unsigned hardware_concurrency() noexcept;

▶️std::call_once

call_once 函数保证一个函数在多线程环境下仅仅调用一次

// 定义于头文件 <mutex>
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

示例代码如下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

once_flag g_flag;
void do_once(int a, string b)
{
    cout << "name: " << b << ", age: " << a << endl;
}

void do_something(int age, string name)
{
    static int num = 1;
    call_once(g_flag, do_once, 19, "luffy");
    cout << "do_something() function num = " << num++ << endl;
}

int main()
{
    thread t1(do_something, 20, "ace");
    thread t2(do_something, 20, "sabo");
    thread t3(do_something, 19, "luffy");
    t1.join();
    t2.join();
    t3.join();

    return 0;
}

示例代码输出:

name: luffy, age: 19
do_something() function num = 1
do_something() function num = 2
do_something() function num = 3

🏅Mutex

std::mutex 定义于头文件,线程同步工具之一,主要用于多线程并发问题,即多个线程同时访问同一个内存区域造成的混乱问题
c++11中提供了四种互斥锁:

  • std::mutex 独占的互斥锁,不能递归使用
  • std::timed_mutex 带超时的独占互斥锁,不能递归使用
  • std::recursive_mutex 递归互斥锁,不带超时
  • std::recursive_timed_mutex 带超时的递归互斥锁

▶️成员函数

void lock();
bool try_lock();
void unlock();

lock():互斥锁有两种状态锁定未锁定状态,当互斥锁处于未锁定状态时,调用lock函数获得锁的所有权并上锁,其他线程
得不到互斥锁的所有权而被lock函数阻塞;当拥有互斥锁所有权的线程解锁,此时其他线程会竞争互斥锁的所有权,竞争得到互斥锁所有权
的线程继续执行,没有得到所有权的线程则继续被lock函数阻塞

try_lock:与lock函数作用类似,但与lock函数不同的是:try_lock不会阻塞线程

  • 如果互斥锁是未锁定状态,获取互斥锁所有权上锁成功返回true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权返回false

▶️线程同步

线程同步思路:

  1. 找到多个线程操作的共享资源(全局变量、堆内存)
  2. 找到操作共享资源的上下文即临界区
  3. 在临界区上边界调用lock函数
  4. 在临界区下边界调用unlock函数

NOTE:线程同步将多线程并行访问


std::atomic

c++ atomic 保证共享变量的操作不受其他线程的影响,避免出现竞态条件和死锁,是c++中一种同步机制

condition_variable

condition_variable是c++ 11提供的线程同步机制,它阻塞一个线程,直到收到另一个线程的通知或者信号,条件变量需要和互斥量配合使用,c++11提供了2种条件变量:

  • condition_variable : 需要配合std::unique_lockstd::mutex使用
    代码示例
#include<iostream>
#include <mutex>
#include <condition_variable>
#include <string>
#include <thread>
std::condition_variable cv;
std::mutex lk;

🏅Chrono时间库

命名空间:std::chrono,常见预定义类型:

  • std::chrono::hours 小时
  • std::chrono::seconds 秒
  • std::chrono::minutes 分钟
  • std::chrono::milliseconds 毫秒
  • std::chrono::microseconds 微秒
  • std::chrono::nanoseconds 纳秒

时间间隔duration

duration表示一段时间间隔,原型定义如下:

//定义于头文件 <chrono>
template<class Rep, class Period = std::ratio<1>> duration;
  • Rep:这是一个数值类型,表示时钟数(周期)的类型(默认为整形)。若 Rep 是浮点数,则 duration 能使用小数描述时钟周期的数目
  • Period:表示时钟得周期,原型定义如下:
// 定义于头文件 <ratio>
template<
    std::intmax_t Num,
    std::intmax_t Denom = 1
> class ratio;

ratio类表示每个时钟周期的秒数,其中第一个模板参数 Num代表分子,Denom代表分母,该分母值默认为 1。因此,ratio代表的是一个分子除以分母的数值

  • ratio<2> 代表一个时钟周期是 2 秒
  • ratio<60 > 代表一分钟
  • ratio<60*60 > 代表一个小时
  • ratio<606024 > 代表一天
  • ratio<1,1000 > 代表的是 1/1000 秒,也就是 1 毫秒
  • ratio<1,1000000 > 代表一微秒,ratio<1,1000000000 > 代表一纳秒

duration的构建原型如下

// 1. 拷贝构造函数
duration( const duration& ) = default;
// 2. 通过指定时钟周期的类型来构造对象
template< class Rep2 >
constexpr explicit duration( const Rep2& r );
// 3. 通过指定时钟周期类型,和时钟周期长度来构造对象
template< class Rep2, class Period2 >
constexpr duration( const duration<Rep2,Period2>& d );

duration提供了获取始终周期数的函数,定义如下:

constexpr rep count() const;

通过构建函数构造时间间隔对象示例如下:

#include <chrono>
#include <iostream>
using namespace std;
int main()
{
    chrono::hours h(1);                          // 一小时
    chrono::milliseconds ms{ 3 };                // 3 毫秒
    chrono::duration<int, ratio<1000>> ks(3);    // 3000 秒

    // chrono::duration<int, ratio<1000>> d3(3.5);  // error
    chrono::duration<double> dd(6.6);               // 6.6 秒

    // 使用小数表示时钟周期的次数
    chrono::duration<double, std::ratio<1, 30>> hz(3.5); //3.5*(1/30)秒
}

具体使用代码示例如下:

#include <chrono>
#include <iostream>
int main()
{
    std::chrono::milliseconds ms{3};         // 3 毫秒
    std::chrono::microseconds us = 2*ms;     // 6000 微秒
    // 时间间隔周期为 1/30 秒
    std::chrono::duration<double, std::ratio<1, 30>> hz(3.5);
    //操作符重载
    std::chrono::minutes t1(10);
    std::chrono::seconds t2(60);
    std::chrono::seconds t3 = t1 - t2; // type::minutes - type::seconds --> type::seconds
    std::cout<< t3.count()<<std::endl;
    /*
    **注意事项:duration 的加减运算有一定的规则,当两个 duration 时钟周期不相同的时候,
    **会先统一成一种时钟,然后再进行算术运算,
    **统一的规则如下:假设有 ratio<x1,y1> 和 ratio<x2,y2 > 两个时钟周期,
    **首先需要求出 x1,x2 的最大公约数 X,然后求出 y1,y2 的最小公倍数 Y,
    **统一之后的时钟周期 ratio 为 ratio<X,Y>。
    */
    std::chrono::duration<double,std::ratio<9,7>> d1(3);
    std::chrono::duration<double,std::ratio<6,5>> d2(1);
    std::chrono::duration<double,std::ratio<3,35>> d3 = d1 - d2;
    //对于分子6、9的最大公约数为3,分母7、5的最小公倍数35,因此推导出d3的ratio为std::ratio<3,35>
    
    std::cout <<  "3 ms duration has " << ms.count() << " ticks\n"
              <<  "6000 us duration has " << us.count() << " ticks\n"
              <<  "3.5 hz duration has " << hz.count() << " ticks\n";       
}

时间点time point

定义原型如下:

// 定义于头文件 <chrono>
template<
    class Clock,
    class Duration = typename Clock::duration
> class time_point;

构建函数如下:

// 1. 构造一个以新纪元(epoch,即:1970.1.1)作为值的对象,需要和时钟类一起使用,不能单独使用该无参构造函数
time_point();
// 2. 构造一个对象,表示一个时间点,其中d的持续时间从epoch开始,需要和时钟类一起使用,不能单独使用该构造函数
explicit time_point( const duration& d );
// 3. 拷贝构造函数,构造与t相同时间点的对象,使用的时候需要指定模板参数
template< class Duration2 >
time_point( const time_point<Clock,Duration2>& t);

时钟clocks

chrono中提供了获取当前系统时钟的时钟类,包含的时钟一共有三类:

  • system_clock:系统时钟,系统的时钟可以修改,甚至可以网络对时,因此基于系统时钟计算的时间间隔差可能不准
  • steday_clock:固定时钟,相当于秒表,开始计时后,时间只能增加而不能减少,适合计算程序运行时间
  • high_resolution_clock:和steday_clock等价

system_clock

函数源码定义如下:

struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
    using rep                       = long long;
    using period                    = ratio<1, 10'000'000>; // 100 nanoseconds
    using duration                  = chrono::duration<rep, period>;
    using time_point                = chrono::time_point<system_clock>;
    static constexpr bool is_steady = false;
    //返回当前时间点
    _NODISCARD static time_point now() noexcept 
    { // get current time
        return time_point(duration(_Xtime_get_ticks()));
    }

    _NODISCARD static __time64_t to_time_t(const time_point& _Time) noexcept 
    { // convert to __time64_t
        return duration_cast<seconds>(_Time.time_since_epoch()).count();
    }

    _NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept 
    { // convert from __time64_t
        return time_point{seconds{_Tm}};
    }
};

示例使用代码如下:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 新纪元1970.1.1时间
    system_clock::time_point epoch;

    duration<int, ratio<60*60*24>> day(1);
    // 新纪元1970.1.1时间 + 1天
    system_clock::time_point ppt(day);

    using dday = duration<int, ratio<60 * 60 * 24>>;
    // 新纪元1970.1.1时间 + 10天
    time_point<system_clock, dday> t(dday(10));

    // 系统当前时间
    system_clock::time_point today = system_clock::now();
    
    // 转换为time_t时间类型
    time_t tm = system_clock::to_time_t(today);
    cout << "今天的日期是:    " << ctime(&tm);

    time_t tm1 = system_clock::to_time_t(today+day);
    cout << "明天的日期是:    " << ctime(&tm1);

    time_t tm2 = system_clock::to_time_t(epoch);
    cout << "新纪元时间:      " << ctime(&tm2);

    time_t tm3 = system_clock::to_time_t(ppt);
    cout << "新纪元时间+1天:  " << ctime(&tm3);

    time_t tm4 = system_clock::to_time_t(t);
    cout << "新纪元时间+10天: " << ctime(&tm4);
}

示例代码输出为:

今天的日期是:    Thu Apr  8 11:09:49 2021
明天的日期是:    Fri Apr  9 11:09:49 2021
新纪元时间:      Thu Jan  1 08:00:00 1970
新纪元时间+1天:  Fri Jan  2 08:00:00 1970
新纪元时间+10天: Sun Jan 11 08:00:00 1970

steday_clock

源码定义如下所示:

struct steady_clock { // wraps QueryPerformanceCounter
    using rep                       = long long;
    using period                    = nano;
    using duration                  = nanoseconds;
    using time_point                = chrono::time_point<steady_clock>;
    static constexpr bool is_steady = true;

    // get current time
    _NODISCARD static time_point now() noexcept 
    { 
        // doesn't change after system boot
        const long long _Freq = _Query_perf_frequency(); 
        const long long _Ctr  = _Query_perf_counter();
        static_assert(period::num == 1, "This assumes period::num == 1.");
        const long long _Whole = (_Ctr / _Freq) * period::den;
        const long long _Part  = (_Ctr % _Freq) * period::den / _Freq;
        return time_point(duration(_Whole + _Part));
    }
};

示例代码求函数的运行时间耗时:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 获取开始时间点
    steady_clock::time_point start = steady_clock::now();
    // 执行业务流程
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
    // 获取结束时间点
    steady_clock::time_point last = steady_clock::now();
    // 计算差值
    auto dt = last - start;
    cout << "总共耗时: " << dt.count() << "纳秒" << endl;
}

转换函数 duration_cast

duration_cast是chrono提供的修改duration内部的时钟周期priod和周期次数rep,该函数原型如下:

template <class ToDuration, class Rep, class Period>
  constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);
  1. 如果对时钟周期进行修改,则源时钟周期必须能整除目标时钟周期(比如小时到分钟)
  2. 如果对周期次数进行修改,窄字符宽度默认可以向宽字符精度转换(int转到float/double)
  3. 如果时钟周期和周期次数都改变,则根据第二点来推导
  4. 如果以上都不满足,需要利用duration_cast进行转换

示例代码如下:

#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;

void f()
{
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
}

int main()
{
    auto t1 = steady_clock::now();
    f();
    auto t2 = steady_clock::now();

    // 整数时长:时钟周期纳秒转毫秒,要求 duration_cast
    auto int_ms = duration_cast<chrono::milliseconds>(t2 - t1);

    // 小数时长:不要求 duration_cast
    duration<double, ratio<1, 1000>> fp_ms = t2 - t1;

    cout << "f() took " << fp_ms.count() << " ms, "
        << "or " << int_ms.count() << " whole milliseconds\n";
}

示例代码输出:

print 1000 stars ....
****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
f() took 40.2547 ms, or 40 whole milliseconds
posted @   DumoIO  阅读(162)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示