首页 > 编程语言 >C++ - 多线程之线程同步

C++ - 多线程之线程同步

时间:2023-10-11 17:15:19浏览次数:36  
标签:include foo lock C++ 互斥 mutex 线程 多线程

1.多线程的并发问题

线程间为什么需要同步?直接来看一个例子:

int a = 0;
void foo()
{
	for (int i = 0; i < 10000000; ++i)
	{
		a += 1;
	}
}
int main()
{
	clock_t start, end;
	start = clock();
	thread t1(foo);
	thread t2(foo);
	t1.join();
	t2.join();
	end = clock();
	cout << a << endl;
	cout << end - start << endl;
	return 0;
}

代码很简单,创建两个线程执行foo函数,foo函数的功能是对全局变量a进行自增,我们所预期的答案是20000000。但是实际运行结果却几乎不可能得到这个值,运行结果如下:

a的最终结果为10442732,共使用了58毫秒的时间。在两个线程对a进行自增的过程中可能会因为线程调度的问题使得最终结果并不正确。比如当前a的值为1,线程x现在将a的值读到寄存器中,而线程y也将a读到寄存器中,完成了自增并将新的值放入内存中,现在a的值为2,而线程x现在也对寄存器中的值进行自增,并将得到的结果放入内存中,a的值为2。可以看到两个线程都对a进行了自增,但是却得到的错误的结果。
这种情况便需要对线程间进行同步。

 

2.C++11的线程间同步方式

其实在APUE的学习中已经讲过了线程间的同步方式,共有五种,分别是互斥锁,自旋锁,读写锁,条件变量和屏障。 但是unix系统中的同步方式编写的代码并不能跨平台,都是C语言风格的结构,使用起来并不方便。所以在后来编写代码的过程中更喜欢使用C++11线程库和同步方式,不仅接口简单,而且也能在夸平台上使用。  

2.1 互斥锁

mutex _mutex;
_mutex.lock();//加锁
_mutex.unlock();//解锁
_mutex.try_lock();//尝试加锁,成功返回bool,失败返回false不阻塞

包含mutex头文件后,就可以使用mutex类。相比起unix风格(接口名字复杂,且需要初始化互斥锁)要方便不少。

现在使用互斥锁来实现两个线程对同一变量自增的功能:

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

int a = 0;
mutex _mutex;
void foo()
{
	for (int i = 0; i < 10000000; ++i)
	{
		_mutex.lock();
		a += 1;
		_mutex.unlock();
	}
}
int main()
{
	clock_t start, end;
	start = clock();
	thread t1(foo);
	thread t2(foo);
	t1.join();
	t2.join();
	end = clock();
	cout << a << endl;
	cout << end - start << endl;
	return 0;
}

只有获得锁之后才能对数据a进行操作,运行结果如下:

 

可以看到这次得到了我们期望的正确答案,但是使用的时间却大大增加,使用了2661ms,是之前的40倍。造成这种现象的原因:   锁的争用造成了线程阻塞 互斥锁的获取需要陷入内核态,即每次上锁解锁,都需要从用户态进入内核态,再回到用户态。而foo函数本身执行自增操作只需要两条指令就能完成,而内核态的切换可能需要上百条指令。 要实现更加高效的同步就需要引入下一个内容,自旋锁。

 

2.2 自旋锁

自旋锁是一种忙等形式的锁,会再用户态不同的询问锁是否可以获取,不会陷入到内核态中,所以更加高效。缺点是可能会对CPU资源造成浪费。但是在C++11中并没有直接提供自旋锁的实现。但是在C++11中提供了原子操作的实现,可以借助原子操作实现简单的自旋锁。

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

atomic_flag flag;
int a = 0;

void foo()
{
	for (int i = 0; i < 10000000; ++i)
	{
		while (flag.test_and_set())
		{

		}//加锁
		a += 1;
		flag.clear();//解锁
	}
}

int main()
{
	flag.clear();//初始化为clear状态

	clock_t start, end;
	start = clock();
	thread t1(foo);
	thread t2(foo);
	t1.join();
	t2.join();
	end = clock();
	cout << a << endl;
	cout << end - start << endl;
	return 0;
}
atomic_flag是一个原子变量,共有set和clear两种状态。在clear状态下test_and_set会将其状态置于set并返回false,在set状态下test_and_set会返回true。可以看到自旋锁其实和CAS方式实现的乐观锁很相似,使用原子操作改变标志为的值,并不断地轮询标志位。   运行结果如下: 可以看到得到了预期的正确答案,但是在时间性能上,并没有比互斥锁高出特别多。感觉很疑惑,查了一些资料,找到一个比较合理的解释:现代操作系统中的互斥锁是一种更加综合的锁,是互斥锁和自旋锁的结合,在无法获得锁时会先自旋一段时间,如果在这段时间中获得了锁便继续执行,如果没有获得便陷入内核阻塞进程。   自旋锁和互斥锁的优缺点和使用场景:
  1. 互斥锁不会浪费CPU资源,在无法获得锁时使线程阻塞,将CPU让给其他线程使用。比如多个线程使用打印机等公共资源时,应该使用互斥锁,因为等待时间较长,不能让CPU长时间的浪费。
  2. 自旋锁效率更高,但是长时间的自旋可能会使CPU得不到充分的应用。在临界区代码较少,执行速度快的时候应该使用自旋锁。比如多线程使用malloc申请内存时,内部可能使用的是自旋锁,因为内存分配是一个很快速的过程。
 

2.3 条件变量

条件变量在C++11中有现成的类可以使用,比unix风格的接口更加方便。用法和unix的条件变量类似,需要配合互斥锁使用。 如果不懂条件变量原理及使用的可以看看这篇博客:C++条件变量实现多线程顺序打印   现在我们使用C++11的条件变量完成三个线程顺序打印0,1,2:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

condition_variable cond;
mutex _mutex;
int a = 0;

void first() {


	while (true)
	{
		unique_lock<mutex> lck(_mutex);
		while (a % 3 != 0)
			cond.wait(lck);
		a++;
		printf("%d\n", a % 3);
		cond.notify_all();
		lck.unlock();
	}
}
void second() {

	while (true) {
		unique_lock<mutex> lck(_mutex);
		while (a % 3 != 1)
			cond.wait(lck);
		a++;
		printf("%d\n", a % 3);
		cond.notify_all();
	}
}
void third() {

	while (a < 100) {
		unique_lock<mutex> lck(_mutex);
		while (a % 3 != 2)
			cond.wait(lck);
		a++;
		printf("%d\n", a % 3);
		cond.notify_all();
	}
}

int main()
{
	thread t1(first);
	thread t2(second);
	thread t3(third);
	getchar();
	return 0;
}

运行结果如下:

其中unique_lock是对mutex的一种RAII使用手法,来看看unique_lock的构造函数和析构函数:

explicit unique_lock(_Mutex& _Mtx)
	: _Pmtx(_STD addressof(_Mtx)), _Owns(false)
	{	// construct and lock
	_Pmtx->lock();
	_Owns = true;
	}
~unique_lock() noexcept
	{	// clean up
	if (_Owns)
		_Pmtx->unlock();
	}		
可以看到在unique_lock构造时自动加锁,析构时完成锁的释放,使用这种编程技法可以保证在推出临界区时锁一定会被释放。     

2.4 屏障

屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。   但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。   不过屏障本身很少被使用,可以使用条件变量和互斥锁完成屏障的功能:    
#include <iostream>
#include <thread>
#include <mutex>
#include <Windows.h>
using namespace std;

condition_variable cond;
mutex _mutex;
//unique_lock<mutex> lck(_mutex);
int _latch = 3;

void wait()
{
	cout << "wait..." << endl;
	unique_lock<mutex> lck(_mutex);
	while (_latch != 0)
		cond.wait(lck);
}
void CountDown()
{
	unique_lock<mutex> lck(_mutex);
	_latch -= 1;
	if (_latch == 0)
		cond.notify_all();
}
void thread1()
{
	Sleep(1000);
	CountDown();
	cout << "thread1 finish" << endl;
}
void thread2()
{
	Sleep(3000);
	CountDown();
	cout << "thread2 finish" << endl;
}
void thread3()
{
	Sleep(5000);
	CountDown();
	cout << "thread3 finish" << endl;
}
int main()
{
	//Sleep(5000);
	thread t1(thread1);
	thread t2(thread2);
	thread t3(thread3);
	wait();

	t1.join();
	t2.join();
	t3.join();
	cout << "all thread finish" << endl;

	return 0;
}

运行结果如下:

完成了屏障的功能,当线程123都执行完成后,main线程才会继续执行。

除去上述的同步方式之外,unix系统还提供读写锁用于线程同步,在C++11中也没有对应的接口,不过实现较为复杂

 

 

标签:include,foo,lock,C++,互斥,mutex,线程,多线程
From: https://www.cnblogs.com/zhuchunlin/p/17757650.html

相关文章

  • C++ - VS2019配置pthread线程库
    1.说明在VS里用MS编译器不能直接调用pthread库,需要先自行下载该库:http://sourceware.org/pub/pthreads-win32/pthreads-w32-2-9-1-release.zip解压后用的到的只有Pre-built.2文件夹下的文件。 2.配置如下图分别配置三大项:包含目录-->...pthreads-w32-2-9-1-release\Pre-......
  • C++ - TCP通信
    1.前言socket编程分为TCP和UDP两个模块,其中TCP是可靠的、安全的,常用于发送文件等,而UDP是不可靠的、不安全的,常用作视频通话等。如下图:1.1头文件与库:#include<WinSock2.h>​#pragmacomment(lib,"ws2_32.lib")1.2准备工作:创建工程后,首先右键工程,选择属性然后选择......
  • C++ - UDP通信
    1.UDP通信流程UDP就比较简单了,步骤比tcp要少一些。连接过程图:  1.1服务器1.初始化套接字库WORDwVersion;WSADATAwsaData;interr;​wVersion=MAKEWORD(1,1);2.创建套接字SOCKETsockSrv=socket(AF_INET,SOCK_DGRAM,0);3.绑定//SOCKADDR_INaddrSrv......
  • C++ - 连接mysql数据库
    1.准备工作1.1把libmysql.dll和libmysql.lib文件复制到工程目录下首先,我们要找到刚刚开始下载的MySQL数据库的安装目录,打开目录,并且将libmysql.dll文件和libmysql.lib文件复制到工程目录下~我安装MySQL的路径:E:\mysql-5.7.42-winx64\lib把libmysql.dll文件和l......
  • C++ - 操作mysql数据库
    操作数据库的案例#include<stdio.h>#include<stdlib.h>#include<mysql.h>//固定不变的MYSQLmysql;//一个数据库结构体MYSQL_RES*res;//一个结果集结构体MYSQL_ROWrow;//char**二维数据,存放一条条记录voidconnect();//连接数据库voidinsert();//插入......
  • WebAssembly C++开发环境搭建
    WebAssembly开发环境搭建简介WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行-它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C/C++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两......
  • c++编译报错解决办法
    所有的警告都被当作是错误在全局域:cc1plus:错误:unrecognizedcommandlineoption‘-Wno-unknown-warning’[-Werror]cc1plus:所有的警告都被当作是错误解决办法:去除CMakeLists.txt中的-Werror......
  • libuv多线程简单应用示范
     #include<stdio.h>#include<uv.h>//声明回调函数voidasyncCallback(uv_async_t*handle);voidclose_cb();voidthread_func_1(void*arg);voidthread_func_2(void*arg);//定时器回调函数voidtimer_callback(uv_timer_t*handle){//定时器触发后执......
  • C++ - 文件读写
    5文件操作 程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放通过文件可以将数据持久化C++中对文件操作需要包含头文件==<fstream>== 文件类型分为两种:文本文件-文件以文本的ASCII码形式存储在计算机中二进制文件-文件以文本的二进制形式存储......
  • C++ - 模板
     本阶段主要针对C++泛型编程和STL技术做详细讲解,探讨C++更深层的使用1模板1.1模板的概念模板就是建立通用的模具,大大提高复用性 模板的特点:模板不可以直接使用,它只是一个框架模板的通用并不是万能的 1.2函数模板 C++另一种编程思想称为==泛型编程......