c++小技巧14:多线程

news2024/11/22 17:02:32

1.前言

最近,被期末考试AK的zzb在回顾以前的代码时,无意看到一个问题:

请问:

大佬能解释一下怎么同时运行两个c++for循环吗?
就比如说游戏里你一边出招电脑也能出招这种的

 当时,zzb是用的kd来解决(详见小技巧2)

而现在,zzb要用一种新的方式:

多线程

2.正文

1.定义

摘自百度百科

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 

所以,上述问题用c++就可以很好解决

2.thread

1.定义

thread是c++多线程中的一种

在c++11时,头文件

#include<thread>

就被加入

该头文件中定义了thread类,创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。

看着很绕对不对?

用样例解释一下

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

void zzb(int a){cout<<"这是线程"<<a<<"\n";}

int main()
{
    cout<<"主线程:"<<endl;
    //定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
    cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id 
    th2.join();//暂停主线程,运行th2 
    return 0;
}

运行出来的效果可能

当然,也可能是

甚至种种

(大致是因为线程运行的时间不定吧)

在网上看到一个生动的例子:

你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。

而代码里面也用到了一些成员函数:

2.成员函数

函数名作用
get_id获取线程 ID
joinable(bool)检查线程是否可被 join,如果线程未被joindetach则返回true
join阻塞(暂停)当前线程直到join的线程返回
detach不阻塞当前线程,不等待该线程返回,相当于这是个守护线程。
swap交换线程
native_handlee,点这里
hardware_concurrencye,检测硬件并发特性(后两个zzb都不会awa)

3.创建线程

1里面是直接创建,再来一遍加深记忆

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

void zzb(int a){cout<<"\n这是线程"<<a<<"\n";}

int main()
{
    cout<<"主线程:"<<endl;
    //定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
    cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id 
    th2.join();//暂停主线程,运行th2 
    return 0;
} 

除此之外,匿名函数lambda也可以

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

int main()
{
	auto zzb=[](int a){cout<<"这是线程"<<a<<"\n";};
	//定义 线程名 函数名 参数 
    thread th2     (zzb,   9);
	th2.join();
	return 0;
}

class也可以

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

class node
{
	public:
		void zzb(int a){cout<<"这是线程"<<a<<"\n";}
}a;

int main()
{
    thread th2(&node::zzb,&a,9);
	th2.join();
	return 0;
}

4.互斥量

1里面出现了一个神奇的bug

而对于这个bug,互斥量就可以解决

比方:

这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。

互斥量在

#include<mutex>

 里面

用法:

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
    m.lock();//许可
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
    m.unlock();//归还,一定要写,不然TLE
}

void p2(int a)
{
    m.lock();
    cout<<"p2函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
    m.unlock();
}

void p3(int a)
{
    cout<<"p3函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}

void p4(int a)
{
    cout<<"p4函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    system("pause");
    thread pr3(p3,a);
    thread pr4(p4,a);
    pr3.join();
    pr4.join();
    return 0;
}

效果很明显:

前面很合理,后面很核理

再详细说一下互斥量函数

5.mutex

1.lock

调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:

1.如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。

2.如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。

3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)(说白了TLE)。

2.unlock()

解锁,释放对互斥量的所有权,如果没有锁的所有权尝试解锁会导致程序异常。

3 try_lock()

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:

1.如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
2.如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

4.lock_guard()

e,一个局部对象,对象内自动+-lock

例子:

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();
	//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void p2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout<<"p2函数正在改写a"<<endl;
        cout<<"原始a为"<<a<<endl;
        cout<<"现在a为"<<a+1<<endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout<<"作用域外的内容3"<<endl;
    cout<<"作用域外的内容4"<<endl;
    cout<<"作用域外的内容5"<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

当然,lock_guard传两个参数时,如果第个为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。(不然位置还是要乱)

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

mutex m;//实例化m对象,不要理解为定义变量

void p1(int a)
{
	m.lock(); 
    lock_guard<mutex> g1(m,adopt_lock);//用此语句替换了m.lock();
	//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout<<"p1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

void p2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout<<"p2函数正在改写a"<<endl;
        cout<<"原始a为"<<a<<endl;
        cout<<"现在a为"<<a+1<<endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout<<"作用域外的内容3"<<endl;
    cout<<"作用域外的内容4"<<endl;
    cout<<"作用域外的内容5"<<endl;
}

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

当然,比起lock_guard,unique_lock更强

除了adopt_lock,它还支持try_to_lock/defer_lock

别的一样(所以谁还用lock_guard?)

try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;

例子:

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

mutex m;
void p1(int a)
{
    unique_lock<mutex> g1(m,defer_lock);//始化了一个**没有加锁**的mutex
    cout<<"关注一下吧"<<endl;
    g1.lock();//手动加锁;
	//注意,不是m.lock();
	//注意,不是m.lock();
	//注意,不是m.lock()
    cout<<"proc1函数正在改写a"<<endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+2<<endl;
    g1.unlock();//临时解锁
    cout<<"祝关注的同学"<<endl;
    g1.lock();
    cout<<"AKIOI,暴打集训队"<<endl;
}//自动解锁

void p2(int a)
{
    unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
    cout<<"proc2函数正在改写a" << endl;
    cout<<"原始a为"<<a<<endl;
    cout<<"现在a为"<<a+1<<endl;
}//自动解锁

int main()
{
    int a=0;
    thread pr1(p1,a);
    thread pr2(p2,a);
    pr1.join();
    pr2.join();
    return 0;
}

(还是会错位,可能因为临时解锁时可能正好try_to_lock try到了)

繁琐是很繁琐,但用的时候还是真香

recursive_mutex

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权

放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同

其余=mutex

time_mutex

比mutex多了两个函数

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

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

timed_mutex m;

void p1()
{
	//等lock: 每随机ms输出一次关注(还不关注) 
	while(!m.try_lock_for(std::chrono::milliseconds(rand()%200+1))) cout<<"关注";
	//得到lock,等1s后输出点赞(还不点赞)
	this_thread::sleep_for(std::chrono::milliseconds(1000));
	cout<<"点赞\n";
	m.unlock();
}

int main()
{
	srand(time(NULL));
	std::thread t[5];//线程组
	for(int i=0;i<5;i++)t[i]=thread(p1);
	for(auto& th:t)th.join();
	return 0;
}

recursive_timed_mutex

e,你看一下recursive_mutex与mutex的区别,再类比一下(不会问老师什么是类比)

(没写完的下次写,肝不动了)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GO 的那些 IDE

文章目录 支持哪些功能快捷键代码高亮代码格式化代码提示导航跳转代码调试构建编译其他功能 GO有哪些IDEGolandVS CodeVim GOSublime TextAtomLiteIDEEclipse 总结 “程序员为什么要使用 IDE”&#xff0c;在一些社区论坛&#xff0c;经常可以看到这样的提问。关于是否应该使用…

AI创作之旅:探索提示工程的奇妙世界

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在当今信息爆炸的时代&#xff0c;人工智能的发…

ELK分离式日志(2)

目录 一.FilebeatELK 部署 开台服务器&#xff08;192.168.233.50&#xff09;下载fliebeat&#xff1a; 安装nginx后查看下日志文件&#xff1a; 设置 filebeat 的主配置文件: 关闭logstash&#xff0c;检测文件&#xff1a; 在50节点上启动filebeat&#xff1a; 访问页…

前端基础(三十八):iframe通信、浏览器跨窗口通信

iframe通信 - MessageChannel <!-- index.html --> <h3>MessageChannel</h3> <input id"input" type"text" oninput"handleInput(this.value)" /> <hr /> <iframe src"./demo.html"></iframe&…

Java 数据结构篇-实现红黑树的核心方法

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 红黑树的说明 2.0 红黑树的特性 3.0 红黑树的成员变量及其构造方法 4.0 实现红黑树的核心方法 4.1 红黑树内部类的核心方法 &#xff08;1&#xff09;判断当前…

k8s中服务器容器tcp连接数量优化

netty的http1服务器在运行一段时间后会无法提供服务&#xff0c;返回客户端socket hang up 使用apipost测试抓包显示三次握手后被reset 客户端使用了大量短连接&#xff0c;如果能改成长连接就会消耗更少的连接&#xff0c;但是客户端逻辑无法掌控&#xff0c;只能修改服务器。…

网络安全概述---笔记总结

网络安全概述 网络安全---Cyberspace security 2003年美国提出网络空间的概念 --- 一个由信息基础设施组成的互相依赖的网络。我国官方文件定义&#xff1a;网络空间为继海&#xff0c;陆&#xff0c;空&#xff0c;天以外的第五大人类活动领域 发展阶段&#xff1a; 通信保…

学习STM32,该用哪款开发工具?

有很多初学者在问&#xff1a;学习STM32&#xff0c;该用哪款开发工具&#xff1f; 我首先说一下我的观点&#xff1a; 1.没有最好&#xff0c;只有适不适合&#xff0c;适合自己的才是最好的。 2.开发工具很多&#xff0c;各有各的特点&#xff0c;有优点肯定也有缺点。 本文…

MySQL InnoDB 底层数据存储

InnoDB 页记录Page Directory记录迁移 页 是内存与磁盘交互的基本单位&#xff0c;16kb。 比如&#xff0c;查询的时候&#xff0c;并不是只从磁盘读取某条记录&#xff0c;而是记录所在的页 记录 记录的物理插入是随机的&#xff0c;就是在磁盘上的位置是无序的。但是在页中…

vscode连不上虚拟机,一直密码错误

最近在做毕设&#xff0c;但是vscode使用连接不上虚拟机&#xff0c;我以为是网络配置的问题&#xff0c;一顿查阅没找到原因。 后来查了一下ssh的日志&#xff0c;发现ssh有消息&#xff0c;但是也提示密码错误。 没找到密码配置格式什么的&#xff0c;经查看sshd配置文件发现…

easyexcel导入合并单元格解析(纵向合并,横向合并都支持)

1、按照开发逻辑&#xff0c;首先定义导入接收实体 package com.wang.test.excel;import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data;/***/ Data public class ExcelData1 {ExcelProperty(index 0)private String name;ExcelProperty(index 1)private S…

[C++] external “C“的作用和使用场景(案例)

C中extern "C"的作用是什么&#xff1f; 在 C 中&#xff0c;extern "C" 的作用是告诉编译器按照 C 语言的规范来处理函数名和变量名。这是因为 C 编译器会对函数名和变量名进行名称修饰(name mangling)&#xff0c;以区分不同的函数和变量。而在 C 语言中…

企业需要的3种供应商管理解决方案

传统的 "管理和监控 "供应商管理解决方案是对时间和金钱的巨大浪费。准入、资格认证和细分等孤立的供应商管理流程无法与其他采购流程整合在一起。 此外&#xff0c;从多个来源获取和管理供应商数据的过程只会增加固有的复杂性。而且&#xff0c;人工操作往往会延误…

【Java IO】设计模式 (装饰者模式)

Java I/O 使用了装饰者模式来实现。 装饰者模式 请参考装饰者模式详解 装饰者(Decorator)和具体组件(ConcreteComponent)都继承自组件(Component)&#xff0c;具体组件的方法实现不需要依赖于其它对象&#xff0c;而装饰者组合了一个组件&#xff0c;这样它可以装饰其它装饰者…

Linux中文件IO(open、read、write、close函数使用)

介绍 什么是文件IO&#xff1f; 是操作系统提供的API接口函数。 POSIX接口 &#xff08;了解&#xff09; 注意&#xff1a;文件IO不提供缓冲机制 文件IO的API open close read read 文件描述符概念&#xff1a; 英文&#xff1a;缩写fd&#xff08;file descriptor&#xff…

前端和后端之间的CORS 跨域和解决办法

什么是CORS&#xff08;Cross-Origin Resource Sharing&#xff0c;跨源资源共享&#xff09; 跨源资源共享&#xff08;CORS&#xff0c;或通俗地译为跨域资源共享&#xff09;是一种基于 HTTP 头的机制&#xff0c;该机制通过允许服务器标示除了它自己以外的其他源&#xff0…

go 语言中 json.Unmarshal([]byte(jsonbuff), j) 字节切片得使用场景

struct_tag的使用 在上面的例子看到&#xff0c;我们根据结构体生成的json的key都是大写的&#xff0c;因为结构体名字在go语言中不大写的话&#xff0c;又没有访问权限&#xff0c;这种问题会影响到我们对json的key的名字&#xff0c;所以go官方给出了struct_tag的方法去修改…

【C++杂货铺】三分钟彻底搞懂如何使用C++中max函数

&#x1f308;前言 欢迎收看本期【C杂货铺】&#xff0c;这期内容&#xff0c;我们将围绕C中max函数部分进行讲解&#xff0c;包含了如何查询库函数&#xff0c;max函数的使用方法灯。如果你想学习C&#xff0c;或者刚学完C语言衔接C&#xff0c;那么这篇文章将会非常有效的帮助…

Redis学习——入门篇①

Redis学习——入门篇① 1、2&#xff1a;Redis入门概述3&#xff1a;Redis安装配置10 安装Redis-cli命令 4&#xff1a;Redis——十大数据类型11 redis数据类型12 命令查阅13 key常用命令介绍14 类型大小写和帮助命令15 String 命令&#xff08;上&#xff09;16 String 命令&a…

电脑摄像头设置在哪里?这3种打开方式要记好!

“我需要开一个视频会议&#xff0c;因此可能需要用到电脑的摄像头。但是我不知道我电脑的摄像头设置在哪里&#xff0c;有没有朋友可以告诉我一下呀&#xff1f;” 在日常办公时&#xff0c;我们可能经常要用到电脑摄像头。这不仅为用户提供了实时沟通的机会&#xff0c;还帮助…