C++11 标准库 线程库thread梳理

news/2024/11/15 13:43:11/文章来源:https://www.cnblogs.com/DSCL-ing/p/18302087

目录
  • <thread>
    • this_thread命名空间
      • 1. get_id()
      • 2. sleep_for()
      • 3. sleep_until()
      • 4. yield()
    • thread类
      • 构造函数:
      • 类方法
        • 1. get_id()
        • 2. join()
        • 3. detach()
        • 4. joinable()
        • 5. operator=
        • 6. hardware_concurrency(static)
    • 多线程的两种计算场景

<thread>

this_thread命名空间

在C++11中不仅添加了线程类,还添加了一个关于线程的命名空间std::this_thread,在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。

头文件

image-20240626191048616

1. get_id()

函数原型

thread::id get_id() noexcept;

get_id可以获取主调线程的id,即在主线程中获取到的是主线程id,在子线程中获取到的是子线程id.

注:在VS中,线程id被封装了一层,是一个结构体.

image-20240626191647987

2. sleep_for()

命名空间this_thread中提供了一个休眠函数sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。

这个函数是函数原型如下,参数需要指定一个休眠时长,是一个时间段:

template <class Rep, class Period>void sleep_for (const chrono::duration<Rep,Period>& rel_time);

示例:

        
#include<thread>
#include<chrono>
int main() {std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();std::this_thread::sleep_for(std::chrono::seconds());std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();std::chrono::nanoseconds ret = end - start;std::cout<<"计时器计时时长:"<<ret.count()<<"纳秒"<<ret.count()/ 1000000000<<"秒" << "\n";
}

image-20240626203235095

3. sleep_until()

名空间this_thread中提供了另一个休眠函数sleep_until(),功能和sleep_for几乎类似,区别是sleep_until的参数是时间点.即休眠到某个时间点.

VS中sleep_for调用了sleep_until实现.

函数原型:

template <class Clock, class Duration>void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
#include<thread>
#include<chrono>
int main() {std::cout<<std::this_thread::get_id()<<"\n";std::cout<<"sleep_until 10 seconds after the currend time, start:"<<"\n";std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();std::this_thread::sleep_until(std::chrono::system_clock::now()+std::chrono::seconds(10));std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();std::cout<<"sleep_for:end"<<"\n";std::chrono::nanoseconds ret = end - start;std::cout << "计时器计时时长:" << ret.count() << "纳秒 ~=" << ret.count() / 1000000000 << "秒" << "\n";
}

image-20240626221618591

4. yield()

描述:在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的CPU时间片,最终变为就绪态.线程调用了yield()之后会主动放弃CPU使用权,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况

注意:只是"暂停"继续执行,而不是结束线程运行从头开始

函数原型:

void yield() noexcept;

例程:

#include<iostream>
#include<thread>
#include<chrono>
#include<cstdlib>#pragma warning(disable:4996)void func1() {do {std::cout << std::this_thread::get_id() << "\n";} while (true);
}void func2() {do {std::this_thread::yield();std::cout << std::this_thread::get_id() << "\n";} while (true);
}int main() {std::thread t1(func1);std::thread t2(func2);t1.join();t2.join();return 0;
}

image-20240626224435599

可以发现,线程主动让出时间片后,其他线程竞争力概率提高.(与操作系统调度方式有关,概率问题)

使用场景:

  • 避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降

    在极端情况下,如果当前线程占用CPU资源不释放就会导致其他线程中的任务无法被处理,或者该线程每次都能抢到CPU时间片,导致其他线程中的任务没有机会被执行,此时可以使用yield缓解.

  • 没有满足继续执行的某种条件时,应主动放弃使用权.

thread类

构造函数:

thread() noexcept;  //空线程对象thread( thread&& other ) noexcept;  //移动构造template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args ); //常规构造thread( const thread& ) = delete; //禁止拷贝
  • 线程传非静态成员函数时,语法为

    //法一:
    定义的对象;
    线程对象(&类::成员函数,对象/&对象,参数...);
    

    因为成员函数比较特殊,不能像普通函数那样函数名可以自动转换成指针,必须要取地址&函数名才可以.

    拿到成员函数的地址需要使用到指针和地址相关的操作。在C++中,获取成员函数的地址并调用它是一种相对复杂的过程,因为涉及到了类的实例(对象)和方法的绑定问题。

    例程:

    class A {
    public:static void fun1(){}void fun2(){std::cout<<"func2()"<<"\n"; }
    };int main() {A a;std::thread t1(&A::fun2,a);std::thread t2(&A::fun2,&a); //传地址更优t1.join();t2.join();
    return 0;}
    

    image-20240627221231304

    //法二:
    线程对象(std::bind(&类::成员函数,对象/&对象,参数...),参数...);
    

    image-20240627222131127

  • 静态成员函数语法

    线程对象(&类::成员函数,参数...);
    //静态方法&可加可不加
    

    静态成员函数简单,只需要对函数取地址即可

类方法

1. get_id()

函数原型:

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

thread中的类方法get_id()和this_thread::get_id();功能类似,用于返回thread对象的id.this_thread中的功能是返回主调线程的id.

2. join()

功能:

join()字面意思是连接一个线程,主动地等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调用join()函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。

函数原型:

void join();

为什么需要join:

  • 多线程中,未分离的子线程在结束后会等待将处理结果返回给主线程,如果此时主线程结束,则进程所有资源都会回收,与此同时子线程还在处理数据返回,之后子线程修必然要处理数据,就会发生越界.
  • 同理,线程处理任务过程中,如果主线程结束,资源回收,子线程之后也是非法访问.

join需要注意的几点.

  • 空线程不需要join,因为空线程对象没有任务,并没有创建真实线程(C++线程对象是用于管理系统线程调用)

    join了会报错

    image-20240627195307101

  • 注意,函数体为空的线程是需要调用join方法,因为真实线程被创建了.

  • move后的线程对象也不需要调用join方法,因为资源被转移到别的线程对象了.

3. detach()

描述:

detach()函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。

线程分离函数detach()不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了

函数原型:

void detach();

注意:

  1. detach就算分离,使用的资源依旧属于进程,主线程结束,资源被回收后,detach的子线程如果未执行完毕,之后的行为将是未定义的.在win10中子线程在主线程结束后同步结束.

    主线程结束后,主进程也随着同步退出,资源回收.detach仅仅能起到提前回收的作用.

测试例程:

void func2() {do {std::cout << std::this_thread::get_id() << "\n";_sleep(100);} while (true);
}int main() {std::thread t2(func2);t2.detach();_sleep(1000);
return 0;}

image-20240627201235741

  1. detach后无法再通过线程对象查看子线程id

image-20240627213044034

很少会分离线程,分离后线程控制更加麻烦,需要处理的更多

4. joinable()

函数原型:

bool joinable() const noexcept;

joinable()函数用于判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:

返回值为true:主线程和子线程之间有关联(连接)关系
返回值为false:主线程和子线程之间没有关联(连接)关系

一般情况下,只有线程有任务时,joinable返回真;其他情况,如空线程对象,分离的线程对象以及join之后的线程对象则返回假.

任务执行完,待返回的线程也属于关联状态.

使用场景:

批量创建线程,如果线程后续没有分配任务,在join时程序后报错.因此可以在join前判断是否关联,关联才join,这样程序就不会奔溃.

    std::thread t1;if (t1.joinable() == true){t1.join();}

总结:

  • 在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接
  • 在创建的子线程对象的时候,如果指定了任务函数,子线程启动并执行任务,主线程和这个子线程自动连接成功
  • 子线程调用了detach()函数之后,父子线程分离,同时二者的连接断开,调用joinable()返回false
  • 在子线程调用了join()函数,子线程中的任务函数继续执行,直到任务处理完毕,这时join()会清理(回收当前子线程的相关资源,所以这个子线程和主线程的连接也就断开了,因此,调用join()之后再调用joinable()会返回false。
5. operator=

线程对象不允许拷贝,只能转移

函数原型

// move (1)	
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)	
thread& operator= (const other&) = delete;
6. hardware_concurrency(static)

thread线程类还提供了一个静态方法hardware_concurrency,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。

线程数对应CPU的逻辑核心数,现代个人计算机一般只有一个物理CPU.然后CPU内有多个核心(真多核并行).这些核心能过模拟出多个核心(并发),叫做虚拟核心,计算机中的线程数一般都是虚拟核心数

在操作系统看来,核心数一般就是指虚拟核心数.

以一块8核16线程CPU为例:

一块CPU内有8个核心,每个核心都能模拟出2个虚拟核心,每个核心对应一个线程,即8核16线程.

CPU的核心数和线程数傻傻分不清,一文科普,清晰明了_cpu核心数和线程数-CSDN博客

函数原型:

static unsigned hardware_concurrency() noexcept;

多线程的两种计算场景

1. IO密集型程序
IO密集型程序主要特点是频繁的输入/输出操作,如读写文件、网络通信等。这些操作通常需要花费大量时间等待外部设备完成数据传输,导致CPU空闲。因此,IO密集型程序通常在等待IO操作完成时,会释放CPU资源给其他进程使用。
例如,一个大型文件服务器或数据库系统就是典型的IO密集型程序。在处理大量请求时,它们需要频繁地读写磁盘,使得CPU常常处于空闲状态。
2. CPU密集型程序
相对地,CPU密集型程序主要是进行大量的数学计算、逻辑运算等需要消耗大量CPU资源的任务。这些任务通常不涉及大量的IO操作,而是专注于利用CPU进行高速处理。
例如,科学计算、大数据分析、机器学习等领域的应用就属于CPU密集型程序。它们需要大量的数学计算和逻辑运算,对CPU资源的需求极高。
实现与优化
对于IO密集型程序,关键在于减少不必要的CPU使用,通过多线程、异步IO等技术来提高效率。由于IO操作通常成为性能瓶颈,因此应优先处理这些操作以降低延迟。
而对于CPU密集型程序,优化的重点在于最大化CPU利用率。这可以通过并行计算、多线程等技术实现。通过将任务分解为多个子任务并分配给多个核心同时处理,可以显著提高程序的运行速度。


引用参考 :
https://subingwen.cn/tags/C-11/
https://zh.cppreference.com/w/cpp
https://legacy.cplusplus.com/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/743718.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型中的tempture以及top-p问题

原文:https://blog.csdn.net/jarodyv/article/details/128994176 参考:https://blog.csdn.net/engchina/article/details/138258047 为了使大模型的生成和表达更加多样,GPT采用了tempture以及top-p方法。 GPT的输出层后面一般跟着softmax,在判别式模型中我们会输出概率最高…

Java基础04:数据类型拓展

整型拓展 十进制整数,如:99, -500, 0。 八进制整数,要求以 0 开头,如:015。 十六进制数,要求 0x 或 0X 开头,如:0x15 。 //整型 int i=10; int i2=010; int i3=0x10; System.out.println(i); //10 System.out.println(i2); //8 System.out.println(i3); //16浮点型拓展…

Python 潮流周刊第 2 季完结了,分享几项总结

我订阅了很多的周刊/Newsletter,但是发现它们都有一个共同的毛病:就是缺乏对往期内容的整理,它们很少会对内容数据作统计分析,更没有将内容整理成合集的习惯。 在自己开始连载周刊后,我就想别开生面,除了每周更新正刊外,还计划定期做一些盘点。我认为过往周刊值得被串联…

BUUCTF Crypto 做题记录

目录: 1.一眼就解密 2.Url编码 3.MD5 4.看我回旋踢 5.摩丝 6.password # 1.一眼就解密 https://buuoj.cn/challenges#一眼就解密 ZmxhZ3tUSEVfRkxBR19PRl9USElTX1NUUklOR30= 确实一眼看出是Base64,拿去解密即可2.Url编码 https://buuoj.cn/challenges#Url编码 直接网上搜url …

开源缺陷管理系统全解析:如何选择最适合你的?

国内外主流的10款开源缺陷管理系统软件对比:PingCode、Worktile、Bugzilla、osTicket、MantisBT、Trac、OpenProject、Phabricator、Request Tracker、The Bug Genie。在管理软件项目时,缺陷管理常常是团队面临的一大挑战。选择一个合适的开源缺陷管理系统可以显著提高错误跟…

视野修炼-技术周刊第92期 | 薅牛毛

① YakShaving - 薅牛毛 ② CSS OneLiners ③ Vue Vine - 单文件编写多 Vue 组件 ④ CDN 流量被盗刷经历 ⑤ es-toolkit ⑥ console.log 体验优化 ⑦ 诗境 - 根据图片匹配诗句欢迎来到第 92 期的【视野修炼 - 技术周刊】,下面是本期的精选内容简介 🔥强烈推荐YakShaving - 薅…

1、多线程同步——CPU、core核、线程、内存

CPU 的运行原理 控制单元在时序脉冲的作用下,将指令计数器里所指向的指令地址(这个地址是在内存里的)送到地址总线上去,然后 CPU 将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后 CPU 把数据读到 CPU 的…

基础语法

单行注释 //加内容 多行注释 /内容/ 文档注释/** * */ 标识符Java中所有的组成部分都需要名字.类名,变量以及方法名都被称为标识符 标识符注意点 1.所有的标识符都应该以字母(A-Z或者a-z),美元符$或者下划线_开始 2.首字符之后可以是以字母(A-Z或者a-z),美元符$,下划线_或者数字…

《痞子衡嵌入式半月刊》 第 104 期

痞子衡嵌入式半月刊: 第 104 期这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻,农历年分二十四节气,希望在每个交节之日准时发布一期。 本期刊是开源项目(GitHub: JayHeng/pzh-mcu-bi-weekly),欢迎提交 issue,投稿或推荐你知道的嵌入式那些事儿。 上期回顾 :《…

[深入理解Java虚拟机]HotSpot虚拟机对象

对象的创建 Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:复制、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个…

将虚拟机跑在ceph之中

目录openStack对接ceph1. cinder对接ceph1.1 ceph创建存储池1.2 ceph授权1.3 下发ceph文件1.4 修改globals文件1.5 部署cinder1.6 创建卷null2. nova对接ceph2.1 创建卷2.2 更新cinder权限2.3 修改globals文件2.4 创建目录2.5 升级nova3. 验证nova3.1 检查存储池3.2 创建虚拟机…