cpp多线程(二)——对线程的控制和锁的概念

这篇文章是笔者学习cpp多线程操作的第二篇笔记,没有阅读过第一篇的读者可以移步此处:

Cpp多线程(一)-CSDN博客

如果读者发现我的文章里有问题,欢迎交流哈!

一、如何控制线程呢?

c++11在std::this_thread名称空间(显然,这是一个嵌套在大名称空间里的小名称空间)内定义了一系列公共函数来管理线程。

1、get_id

获得当前线程id

2、yield

将当前线程时间片让渡给其他线程

3、sleep_until

当前线程休眠直到某个时间点

4、sleep_for

当前线程休眠一段时间

单纯看这些定义其实无法准确理解这些函数的意义。实践出真知,下面我们启动小实验来理解这些函数。

实验一:使用get_id
cpp 复制代码
#include <iostream>
#include <thread>

void func1(void)
{
	std::thread::id thread1_id=std::this_thread::get_id();
	std::cout<<thread1_id<<std::endl;
}

int main(void)
{
	std::thread thread1(func1);
	thread1.join();
	std::thread::id mainthread_id=std::this_thread::get_id();
	std::cout<<mainthread_id<<std::endl;
} 

输出结果:

2

1

由此我们发现:

1、get_id函数必须包含在名称空间std::this_thread里

2、get_id函数一般是没有参数的

3、查阅资料发现,get_id函数的返回值的具体类型由编译器定义。但是,任何编译器都定义了std::thread::id这一结构(类)来储存线程id。std::cout函数可以直接打印这一类型

4、在子线程和主线程中均可以使用get_id()

实验二:使用yield

yield函数的使用方法和get_id类似。

调用std::this_thread::yield()时,当前线程会主动放弃执行权,使得操作系统的线程调度器可以重新安排其他可运行的线程来执行。这样做可以提高多线程程序的执行效率和公平性。

需要注意的是,std::this_thread::yield()只是一个建议,具体的线程调度行为取决于操作系统和调度器的实现。在某些情况下,调度器可能会忽略该建议,继续让当前线程执行。

cpp 复制代码
#include <iostream>
#include <thread>
#include <windows.h>

void func1(void)
{
	std::this_thread::yield();
	while(true)
	{
		std::this_thread::yield();
		std::cout<<"thread1"<<std::endl;
		Sleep(1000);
	}
}

void func2(void)
{
	while(true)
	{
		std::cout<<"thread2"<<std::endl;
		Sleep(1000);
	}	
}



int main(void)
{
	std::thread thread1(func1);
	std::thread thread2(func2);
	thread1.join();
	thread2.join();	
} 

输出结果:

thread1thread2

thread2

thread1

thread2thread1

......

可以看到,由于yield的作用,从第二次打印开始,thread2往往比thread1能够先进行打印。而如果注释掉std::this_thread::yield(),那么由于thread1比thread2先定义,thread1往往比thread2先打印。

实验三:使用sleep_for

下面的程序,主线程打印1 2 3,子线程打印4 5 6。为了让打印的数字是有顺序的,使用sleep_for函数让子线程暂停1秒,等待主线程的打印结束。(当然,现实中写程序肯定不会这么设计,我只是在呈现一种最简单的情况)

注意,这里使用了std::chorno::seconds,这是定义在chrono库中的一个类,用于表示时间。

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

void func1(void)
{
	std::this_thread::sleep_for(std::chrono::seconds(1));
	for(int i=4;i<=6;i++)
		std::cout<<i<<std::endl;
}

int main(void)
{
	std::thread thread1(func1);
	for (int i=1;i<=3;i++)
		std::cout<<i<<std::endl;
	thread1.join();
} 

输出结果:

1

2

3

4

5

6

由此我们发现:

1、sleep_for()函数的参数可以是std::chrono::seconds类型,也可以是chrono库中其他时间类型

2、sleep_for()函数没有返回参数

二、互斥锁mutex

1、为什么引入锁这个概念?

先看这段两线程轮流打印的代码:

cpp 复制代码
#include <iostream>
#include <thread>
#include <windows.h>

void func1(void)
{
	while(true)
	{
		std::cout<<"thread1"<<std::endl;
		Sleep(1000);
	}
}

void func2(void)
{
	while(true)
	{
		std::cout<<"thread2"<<std::endl;
		Sleep(1000);
	}	
}

int main(void)
{
	std::thread thread1(func1);
	std::thread thread2(func2);
	thread1.join();
	thread2.join();	
} 

正如我们之前所展示的那样,由于thread1和thread2轮流占用std::cout这一输出流对象,导致输出结果并不是我们想象的那样规整:

thread1

thread2

thread1

thread2

而是会出现这样的情况

thread1thread2

thread1

thread2

所以,我们引入mutex互斥锁这一概念。通过mutex锁的操作,使当thread1使用输出流对象时,把输出流对象锁住。这样,thread2将不被允许使用输出流对象。

cpp 复制代码
#include <iostream>
#include <thread>
#include <windows.h>
#include <mutex>

std::mutex mtx;

void func1(void)
{
	while(true)
	{
		mtx.lock();
		std::cout<<"thread1"<<std::endl;
		Sleep(1000);
		mtx.unlock();
	}
}

void func2(void)
{
	while(true)
	{
		mtx.lock();
		std::cout<<"thread2"<<std::endl;
		Sleep(1000);
		mtx.unlock();
	}	
}

int main(void)
{
	std::thread thread1(func1);
	std::thread thread2(func2);
	thread1.join();
	thread2.join();	
} 

读者可以自行验证上面这段代码,输出结果将非常规整,不会出现上面那种情况。这就使锁所产生的效果。

2、如何理解锁的概念?

如果把内存比作一个个小房间,那么各个线程就要在各个房间里存取数据。如果两个线程同时存取数据,可能会造成一些错误。所以,当线程1在这个房间里存储数据时,就需要把这个房间锁住。线程2若要使用这个房间的数据,就要在外面等待。(具体的方式有线程阻塞,停止执行,即互斥锁 ;也有循环等待直到锁被打开,即自旋锁

3、C++如何定义锁?

c++11的mutex库中定义了std::mutex类,作为操作锁的句柄,用来管理锁的各种行为。mutex类的具体实现对用户封闭,下面只介绍一些对外的接口(API)。了解这些接口(包括mutex的构造函数和公有函数)即可实现锁的操作。

--实例化锁对象

通常把锁对象实例化为全局变量:

cpp 复制代码
#include <mutex>
//mutex类被定义在mutex中

std::mutex mtx;

void func()
{}

int main()
{}
--最简单的上锁和解锁

对mtx使用lock()方法,可以实现上锁操作。使用unlock()方法,可以解锁

假若线程1运行的是func函数,value是定义在全局的变量。

cpp 复制代码
int value=0;
std::mutex mtx;


void func()
{
    mtx.lock();
    value+=1;
    mtx.unlock();
}

那么,在func1访问改写value变量前,添加mtx.lock()语句可以对value变量上锁;此时只有线程1可以访问value,确保了数据安全。改写完毕,使用mtx.unlock()解锁。

可以形象地理解mtx.lock()------这句话就好像给线程1一大把锁和钥匙,线程1要访问哪些变量(特指别的线程也可以访问地公共变量),就在访问时加一把锁,直到访问完成,才把它解开。在此期间,如果线程2也要访问这个变量,就会被暂停.直到线程1处理完毕,线程2才能被唤醒继续执行。

所以,锁这个类写的非常智能,优先拿到锁的线程要访问哪些对象,就把哪些对象锁住。

cpp 复制代码
mtx.lock();
//这中间出现的变量,资源全部被锁保护,别人不许碰!!!
mtx.unlock();
//这之后出现的变量,就不一定是当前线程优先访问了!!!

注意,解锁是非常重要的。试想,你使用一个房间后不解锁,直接翻窗走了(额,有点滑稽。但如果一个函数结束了,就是这样,直接消失),那么这个房间就一直锁住了,别人没有钥匙,不能用了。

--不同线程存在竞争关系
cpp 复制代码
#include <iostream>
#include <thread>
#include <windows.h>
#include <mutex>

std::mutex mtx;

void func1(void)
{
	while(true)
	{
		mtx.lock();
		std::cout<<"thread1"<<std::endl;
		Sleep(1000);
		mtx.unlock();
	}
}

void func2(void)
{
	while(true)
	{
		mtx.lock();
		std::cout<<"thread2"<<std::endl;
		Sleep(1000);
		mtx.unlock();
	}	
}

int main(void)
{
	std::thread thread2(func2);
	std::thread thread1(func1);
	thread1.join();
	thread2.join();	
} 

还是刚刚这一段代码,我们交换线程1和2的注册顺序。发现输出结果就变成了

thread2

thread1

thread2

......

这是由于thread2先注册,可以先抢夺锁的资源,拿到锁后,过河拆桥。不让thread1访问输出流对象了!(当然,thread2访问完毕后,还是要按照规则去唤醒thread1,所以第二个执行的必须是thread1线程)

4、其他方式实现锁

这里介绍mutex库中一个模板类------std::lock_guard。顾名思义,这个类模板实例化后能像保安一样帮我锁门开门(太形象了!)

把mtx.lock()出现的位置使用下面的语句:

cpp 复制代码
std::lock_guard<std::mutex> mylock_guard(mtr);

这是在使用类的构造函数,参数是之前定义的锁mtr。之后,在函数作用域中出现的全局变量和公共资源都会被锁住。

下面的示例代码就用两种方式实现了锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <windows.h>
#include <mutex>

std::mutex mtx;

void func1(void)
{
	while(true)
	{
		std::lock_guard<std::mutex> my_guard(mtx);
		std::cout<<"thread1"<<std::endl;
		Sleep(1000);
	}
}

void func2(void)
{
	while(true)
	{
		mtx.lock();
		std::cout<<"thread2"<<std::endl;
		Sleep(1000);
		mtx.unlock();
	}	
}

int main(void)
{
	std::thread thread2(func2);
	std::thread thread1(func1);
	thread1.join();
	thread2.join();	
} 

可以发现,guard的作用区域是while循环内部。

这就像你雇佣了一群保安大叔,当你进入某个房间(内存)时,帮你锁上门;等你出来,自动帮你把门解锁了(怎么和现实相反了???)。

参考资料:

『面试问答』:互斥锁、自旋锁和读写锁的区别是什么?_哔哩哔哩_bilibili

C++多线程详解(全网最全) - 知乎 (zhihu.com)

相关推荐
娅娅梨24 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控29 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥30 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾33 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我36 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺39 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h1 小时前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20