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

这篇具有很好参考价值的文章主要介绍了cpp多线程(二)——对线程的控制和锁的概念。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

Cpp多线程(一)-CSDN博客

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

一、如何控制线程呢?

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

1、get_id

获得当前线程id

2、yield

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

3、sleep_until

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

4、sleep_for

当前线程休眠一段时间

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

实验一:使用get_id
#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()只是一个建议,具体的线程调度行为取决于操作系统和调度器的实现。在某些情况下,调度器可能会忽略该建议,继续让当前线程执行。

#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库中的一个类,用于表示时间。

#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、为什么引入锁这个概念?

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

#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将不被允许使用输出流对象。

#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、如何理解锁的概念?

cpp多线程(二)——对线程的控制和锁的概念,开发语言,c++

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

3、C++如何定义锁?

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

--实例化锁对象

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

#include <mutex>
//mutex类被定义在mutex中

std::mutex mtx;

void func()
{}

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

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

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

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才能被唤醒继续执行。

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

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

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

--不同线程存在竞争关系
#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()出现的位置使用下面的语句:

std::lock_guard<std::mutex> mylock_guard(mtr);

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

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

#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)文章来源地址https://www.toymoban.com/news/detail-807200.html

到了这里,关于cpp多线程(二)——对线程的控制和锁的概念的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux】多线程 --- 线程概念 控制 封装

    从前种种,譬如昨日死。从后种种,往如今日生。 1.1 进程资源如何进行分配呢?(地址空间+页表) 1. 首先我们来看一个现象,当只有第一行代码时,编译是能通过的,但会报warning,当加了第二行代码时,编译无法通过,报error。 第一行代码能编过的原因是权限缩小,虽然

    2024年02月03日
    浏览(62)
  • 【Linux】多线程1——线程概念与线程控制

    📝 个人主页 :超人不会飞) 📑 本文收录专栏 :《Linux》 💭 如果本文对您有帮助,不妨 点赞、收藏、关注 支持博主,我们一起进步,共同成长! 💭理解线程需要和进程的概念紧密联系。 线程是一个执行分支,执行粒度比进程更细,调度成本更低; 进程是分配系统资源的

    2024年02月12日
    浏览(27)
  • 【Linux】Linux线程概念和线程控制

    线程是进程内的一个执行流。 我们知道,一个进程会有对应的PCB,虚拟地址空间,页表以及映射的物理内存。所以我们把这一个整体看做一个进程,即进程=内核数据结构+进程对应的代码和数据。我们可以这样看待虚存:虚拟内存决定了进程能够看到的\\\"资源\\\"。因为每一个进

    2024年02月04日
    浏览(41)
  • Linux复习 / 线程相关----线程概念与控制 Q&A梳理

    本篇博客梳理关于线程相关的QA,包括了线程概念与线程的控制。若读者也在复习这块知识,或者正在学习这块知识,可以通过这些QA检测自己的知识掌握情况。此外,思维导图已经更新至我的gitee,QA之外的体系梳理还请移步思维导图。 线程概念 Q:线程和进程的区别?(为

    2023年04月14日
    浏览(45)
  • 【探索Linux】—— 强大的命令行工具 P.19(多线程 | 线程的概念 | 线程控制 | 分离线程)

    在当今信息技术日新月异的时代,多线程编程已经成为了日常开发中不可或缺的一部分。Linux作为一种广泛应用的操作系统,其对多线程编程的支持也相当完善。本文将会介绍关于Linux多线程相关的知识,其中包括了线程的概念、线程控制、线程分离等方面的内容。如果你希望

    2024年02月05日
    浏览(50)
  • 『Linux』第九讲:Linux多线程详解(一)_ 线程概念 | 线程控制之线程创建 | 虚拟地址到物理地址的转换

    「前言」文章是关于Linux多线程方面的知识,讲解会比较细,下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句」  我与春风皆过客, 你携秋水揽星河。 ——网络流行语,诗词改版 用现在的话来说:我不

    2024年02月04日
    浏览(45)
  • 多线程Synchronized锁的使用与线程之间的通讯

    多线程同时对同一个全局变量做写操作,可能会受到其他线程的干扰,就会发生线程安全问题。 Java中的全局变量是存放在堆内存中的,而堆内容对于所有线程来说是共享的。 比如下面一个简单的代码案例: 代码比较简单,我们看下面控制台的打印: 可以看到两个线程之间

    2024年02月04日
    浏览(40)
  • 多线程锁的升级原理是什么

            在 Java 中,锁共有 4 种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。         如下图所示         多线程锁的升级过程主要指的是锁从偏向锁状态升级为轻量级

    2024年02月06日
    浏览(36)
  • C# 中多线程锁的使用经验

    C# 中多线程锁的使用经验:全局锁,实例锁         private static object _exeLock = new object();        static 静态的是全应用程序的资源。如果在一个类里定义了这样一个锁,你在调用使用这个类的时候,是NEW了一个对象,并把这个对象给了一个静态全局变量中保存。这时这个锁

    2024年03月14日
    浏览(55)
  • 基于springboot实现多线程抢锁的demo

    1、本代码基于定时调度和异步执行同时处理,如果只加异步处理,会导致当前任务未执行完,下个任务到点也不会触发执行 2、日志信息: 3、使用 jstack 88023 |grep SimpleAsyncTaskExecutor-62 命令可以查看相关线程是否存在,可以看到,相关线程任务执行完成后,会自动消失 4、如果

    2024年02月09日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包