热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C++11多线程编程

因为之前有学习过c11的并发库,最近在搞项目准备复习,本节开始就重温一下这块内容打算连着写上几篇博客去记录一下..题外话get几个概念1.进程是资源分配

因为之前有学习过c11的并发库,最近在搞项目准备复习,本节开始就重温一下这块内容打算连着写上几篇博客去记录一下.. 


题外话get几个概念

1.进程是资源分配的基本单位,线程是调度的基本单位,注意基本二字,这并不意味着进程不能调度,进程当然可以调度;线程是基本的调度单位,只要达到线程水平就可以被调度了

2.目前据我所知哈还没有能直接写出由线程开始的程序,为啥呢?因为此时没有系统资源让你去动,跑程序就要代码,进程是资源分配的基本单位,说明你线程要依附于进程,单单创建个线程,他是没有用户资源区的,就没有代码段数据区这些,不过创建的时候线程他是一直有自己的内核栈的且独立;

3.独有堆栈才能保证自己独立运行,这里的堆栈是共享了父进程的地址空间之后在里面分配了自己独立的堆栈,就是用户态的堆栈吧,还是得依靠父进程,这也能说明了第二个问题,只有依靠了进程,有了用户资源才能跑代码不是


目录

今天先从thread线程的使用开始:

构造函数:

一些公共的api

get_id()

join()

detach()

别的几个函数

 joinable()

hardware_concurrency 

线程的命名空间 std::this_thread

sleep_for()

sleep_until()

yield()

说几个小点

线程独有自己的堆栈

类的成员函数的线程化:



在学习linux的时候肯定都接触过c语言里线程pthread,不过他并不好用,各种回调函数,句柄很多参数等等,相关的api也是一大堆,不过在c++11之后就提供了线程类叫做 std::thread,用起来就很简单了。

说到线程,肯定就要考虑线程同步,线程安全的问题,同linux上一样,c++11也提供了pv原子操作,互斥(各种锁),条件变量,信号量,future(用来获取异步任务)等等这些去支持多线程的并发,后面陆续去说这些类


今天先从thread线程的使用开始:


构造函数:

//
thread() noexcept;
默认构造函,构造一个线程对象,在这个线程中不执行任何处理动作
//
thread( thread&& other ) noexcept;//只能移动构造了
移动构造函数,将 other 的线程所有权转移给新的 thread 对象。之后 other 不再表示执行线程。
//
template
explicit thread( Function&& f, Args&&... args );//可变参数模板
创建线程对象,并在该线程中执行函数 f 中的业务逻辑,args 是要传递给函数 f 的参数
任务函数 f 的可选类型有很多,具体如下:普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型)
可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
//
thread( const thread& ) = delete; //不能拷贝构造
使用 =delete 显示删除拷贝构造,不允许线程对象之间的拷贝

线程中的资源是不能被复制的,因此通过 = 操作符进行赋值操作最终并不会得到两个完全相同的对象。
 

// move (1) //不允许赋值
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete;

如果 other 是一个右值,会进行资源所有权的转移
如果 other 不是右值,禁止拷贝,该函数被显示删除(=delete),不可用



一些公共的api


get_id()

返回线程的id

每个被创建出的线程实例都对应一个线程 ID,这个 ID 是唯一的,可以通过这个 ID 来区分和识别各个已经存在的线程实例


函数原型
std::thread::id get_id() const noexcept;

回收线程资源:有两种

join()和detach()

用的时候只能二选一



join()

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

注意:join在哪个线程里面调用,哪个线程走到join就会阻塞这个线程,比如在main这个主线程去创建了一个子线程funa,funa执行,main也走他的,当走到funa的join时候,main就阻塞在这里了,等着funa走完,返回回来之后main才继续走

很简单的道理

函数原型
void join();

detach()

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

函数原型
void detach();

用了这个函数就好像,孩子叛逆了离家出走跑到后台去了,父母管不着了,父线程管不着子线程了,他们各干各的

对于join和detach个人感觉,用join好一点,毕竟join了父线程必须要等到子线程的结果才行,而detach的话,你也不知道你父线程运行完了,子线程有没有走完,没有走完如果父线程结束程序结束,那结果肯定是由问题的不是~~当然了这是个人见解,学艺不精

给出示例,把上面我说的都演示了一下。

void* funa(void*)
{cout <<"void *" <}
void funb()
{cout <<"void" <}
void func(int a, int b)
{int c = a + b;cout <<"func" <}
int main()
{//创建线程对象thread tha(funa, nullptr);thread thb(funb);thread thc(func, 10, 20);cout <<"funa的线程id:" <}

把join都换成detach,就会发现结果很多次有的子线程的结果都没有完全打印,就是因为父线程结束了,二者分离子线程还没完,除非在父线程结束前你加个sleep休眠一下,等等子线程。像linux下不管是进程fork了或者线程,刚开始学习的时候sleep也是挺常见的操作,毕竟都是些操作系统底层的问题

还有就是你会发现每次打印的结果,经常会乱序,这就是因为线程嘛,不加以控制,大家都在一起走,谁先走完就不确定了。我开篇也说了线程同步,安全这是多线程并发必须要控制的,用互斥,条件变量这些去控制,后面我会分别讲的。。

这两句话算我多啰嗦,能点进来看我这个博客的肯定对于进程和线程的问题,怎么控制这些应该是清楚的。


别的几个函数


 joinable()


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

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


bool joinable() const noexcept;

void foo()
{this_thread::sleep_for(std::chrono::seconds(1));
}int main()
{thread t;cout <<"before starting, joinable: " <}


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

hardware_concurrency 


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


static unsigned hardware_concurrency() noexcept;

 实际上我的电脑本身是4核处理,这个函数返回的其实是虚拟的cpu核心数,也就是逻辑处理器。我这个电脑物理处理器是4,逻辑处理器是8

int main()
{int num = thread::hardware_concurrency();cout <<"CPU number: " <}



线程的命名空间 std::this_thread

在这个命名空间中提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了

get_id()前面说了,说说剩下三个


进程一共有五种状态分别为:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态) 其中创建态和退出态维持的时间是非常短的,稍纵即逝。我们主要是清楚就绪态 , 运行态 , 挂起态

线程创建后同样有这五个状态

sleep_for()

线程和进程的执行有很多相似之处,在计算机中启动的多个线程都需要占用 CPU 资源,但是 CPU 的个数是有限的并且每个 CPU 在同一时间点不能同时处理多个任务。

为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到 CPU 时间片的情况)。

this_thread 中提供了一个休眠函数 sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长因为阻塞态的线程已经让出了 CPU 资源,代码也不会被执行,所以线程休眠过程中对 CPU 来说没有任何负担。

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

template void sleep_for (const chrono::duration& rel_time);

this_thread::sleep_for(chrono::seconds(1));//秒
this_thread::sleep_for(chrono::milliseconds(1000));//毫秒
this_thread::sleep_for(chrono::microseconds(1000*1000));//微妙

注意:程序休眠完成之后,会从阻塞态重新变成就绪态就绪态的线程需要再次争抢 CPU 时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。 

sleep_until()

sleep_until():指定线程阻塞到某一个指定的时间点 time_point类型,之后解除阻塞
sleep_for():指定线程阻塞一定的时间长度 duration 类型,之后解除阻塞

template void sleep_until (const chrono::time_point& abs_time);

sleep_until() 和 sleep_for() 函数的功能是一样的,参数不同,实际开发具体选择

对于这两个函数学习之前要看看C++11 中提供了日期和时间相关的库 chrono,chrono 库主要包含三种类型的类:时间间隔duration、时钟clocks、时间点time point。也就是这俩函数要用到的参数



yield()

在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的 CPU 时间片最终变为就绪态,这样其它的线程就有更大的概率能够抢到 CPU 时间片了。使用这个函数的时候需要注意一点,线程调用了 yield () 之后会主动放弃 CPU 资源,但是这个变为就绪态的线程会马上参与到下一轮 CPU 的抢夺战中,不排除它能继续抢到 CPU 时间片的情况

std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降
std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源,但是在下一轮还会继续抢

void yield() noexcept;


说几个小点


线程独有自己的堆栈

 多线程调用同一个方法,局部变量会共享吗_

//线程函数的返回值是没有意义的,不要取得线程的返回值
void funa(char c)
{cout <}
int main()
{thread tha(funa,&#39;a&#39;);thread thb(funa,&#39;b&#39;);//每个线程独有自己的栈//线程a和b 不共享x这个局部变量,x打印不一样,因为x在不同的系统调用栈中tha.join();thb.join();return 0;
}

局部变量是线程安全的【高并发】终于弄懂为什么局部变量是线程安全的了!! 


类的成员函数的线程化:

class Test {int value;
public:Test() {}~Test(){}static void funa(int a)//没this{cout <<"static funa==" <};
int main()
{thread tha(Test::funa, 10);//不是全局,静态要指明tha.join();int b = 20;Test t;thread thb(&Test::setValue,&t,b);//有this 要指明对象地址thb.join();return 0;
}

class AA {
public:void operator()(int a, int b)//仿函数{int c = a + b;cout <<"c;" <};
int main()
{thread tha(AA(), 10, 20);//仿函数当做可调用对象tha.join();AA aa;thread thb(&AA::mul, &aa, 5, 3);thb.join();int c = 20;//lambda表达式,也可以当线程化的可调动对象thread thc([&](int x)->void {c += x; }, 50);cout <<"c:" <}

class Test {
public:int value;void func(){//thiscout<<"func"<};int main()
{Test test;int* p = nullptr;//0//p = &Test::value;//err//s是个指针,被限定了只能指向Test类型公有数据int Test::* s = nullptr;//s=0xffff ffff,s指的是偏移量s = &Test::value;//okp = &test.value;//ok//s = &test.value;//err,s指的是偏移量,test是具体对象了,就不能这么指向//所以要是类类型的指针,需要指定他的作用域Test t;void(Test:: * pf)() = &Test::func;thread tha(&Test::func,&t);//类型指针,非静态成员的话,需要后面传对象的地址//因为有this指针,静态成员函数就不用tha.join();return 0;
}

好了到这简单回顾了一下线程有关的知识,聊了下C11并发库的这个thread类。看到这线程的使用应该就没问题了,后面我再回顾一下atomic,mutex等等这些去操作线程的类---

ok完事收工。


推荐阅读
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • 本文介绍了在CentOS上安装Python2.7.2的详细步骤,包括下载、解压、编译和安装等操作。同时提供了一些注意事项,以及测试安装是否成功的方法。 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
author-avatar
mobiledu2502855777
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有