【C++11多线程入门教程】之小结

C++11多线程相关功能小结

  前段时间学习并写了些C++11多线程的一些博客知识点,现在基本上入门级的知识算是基本概括了吧。因此,就有了这篇关于C++11多线程的小结。那么我将从之前博客中的知识点抽取一些出来(个人认为是基础使用的进行总结),希望看过有所帮助。关于C++11多线程的知识点如下:

  • 如何创建线程
  • 线程互斥量使用
  • 异步线程操作
  • 原子操作
1. 如何创建线程

C++11抽象一个线程类std::thread,方便我们进行使用。

#include <thread>

int main(void)
{
	std::thread create_thread(create_function);
	
	create_thread.join();
	// create_thread.detach();
	return 0;
}

  上面即为关于如何创建线程对象,调用线程函数等。代码中的create_thread为创建的线程对象,后面括号里面的create_function为线程需要调用的函数。接下来就是线程的类成员函数join()detach()。关于join()就是需要等待线程执行完毕,main()才能够继续往下执行。detach()则是main()函数不需要等待,继续往下执行【使用detach()要考虑清楚再使用】。这里面就谈及最重要的一个线程概念:阻塞。当然,这里面还有joinable()函数,它的主要功能就是检查join()是否调用成功。

【C++11多线程入门教程】系列之线程与进程的基本概念
【C++11多线程入门教程】系列之join()与detach()用法

2. 线程互斥量使用

  互斥量在多线程中是比较重要概念,互斥量的存在主要是为了避免在数据共享过程中的访问冲突问题。那么,互斥量主要就是解决数据共享访问过程中,每个线程函数必须有序进行访问。这里插一句:多个线程只读不改写数据,那么不会存在访问冲突。只要存在某个线程有改写数据访问,那么必须存在互斥量来避免数据访问冲突

互斥量的使用方式:

1 ===> std::mutex

  mutex为最基本的互斥量类,多线程中使用次数频率较高。mutex类主要结合lock()unlock()函数进行对数据访问加锁与解锁。

#include <thread>
#include <mutex>
#include <iostream>

std::mutex mtx;  // 实例化mutex对象

void print(int i)
{
	mtx.lock();  // 加锁
	// ...  // 执行共享数据读写都可以
	mtx.unlock(); // 解锁
}

int main()
{
	int a = 8;
	
	std::thread create(print, a);
	create.join();
	
	return 0;
}

  上述代码简要介绍互斥量的使用,进入线程调用函数print()时候,互斥量对象mtx对其访问数据a进行加锁,退出时候进行解锁。中间的一些省略代表对其进行读写操作都可以。以此来避免该线程执行读写a时候,其它线程无法对其进行访问与修改。一般情况下,我们声明即调用线程函数,因此在下面会使用join()进行阻塞,知道线程函数执行完毕,main函数才继续往下执行。

【C++11多线程入门教程】系列之互斥量mutex
【C++11多线程入门教程】系列之互斥量mutex(补)

2 ===> std::lock_guard

  这里std::lock_guard类如果你看过其源码就知道,主要就是在构造函数里面初始化mutex对象,并自动进行加锁。然后,在析构函数里面对其mutex对象自动进行解锁。主要就是为了解决大家在写加锁lock时候,操作一波数据后,忘记写解锁unlock操作。所以,std::lock_guard类能够有效避免std::mutex带来的问题,因此std::lock_guard类创建就加锁,类对象离开作用域即解锁。合理安排作用域使用std::lcok_guard能够有效避免忘记解锁。

#include <thread>
#include <mutex>
#include <iostream>

std::mutex mtx;  // 实例化mutex对象

void print(int i)
{
	//mtx.lock();  // 加锁
	std::lock_guard<std::mutex> lg(mtx);  // 该语句替换lock()与unlock()
	// ...  // 执行共享数据读写都可以
	//mtx.unlock(); // 解锁
}

int main()
{
	int a = 8;
	
	std::thread create(print, a);
	create.join();
	
	return 0;
}

  上述为基本的std::lock_guard类的使用,当然该lock_guard类有相关参数进行设置。当参数为std::adopt_lock时候,表示构造函数不进行互斥量锁定,只是声明,此时需要手动加锁。无参数时候,lock_guard类默认构造函数调用,声明即加锁。

【C++11多线程入门教程】系列之互斥量lock_guard

3 ===> std::unique_lock

  到这里,已经有个lock_guard为什么还出现个unique_lock类。其实我们上面讲述lock_guard类时候,有说到合理设置作用域来使用lock_guard类进行互斥操作。但是,有些情况下无法有效拆分作用域,但是使用lock_guard又会互斥上锁整个作用域带来性能的下降,这个时候就该unique_lock上场了。

  unique_lock不仅支持原有lock_guard的所有功能,同时还更加灵活的手动进行加锁lock()与解锁unlock(),以此来灵活的限制上锁区域。关于std::unique_lock第二个参数有3种:adopt_lock表示默认假设已经上锁【注意是假设】,try_to_lock表示尝试去锁定,但是必须保证锁处于unlock的状态,然后尝试能否获取加锁;这种方式如果锁成功的话,就锁定该互斥量mutex,如果锁失败也不会造成阻塞,继续往下执行。defer_lock表示初始化一个没有加锁的mutex,后续需要手动加锁。

lock_guardunique_lock
支持手动lock()与unlock()不支持支持
参数adopt_lockadopt_lock、try_to_lock、defer_lock
#include <thread>
#include <mutex>
#include <iostream>

std::mutex mtx;  // 实例化mutex对象

void print(int i)
{
	//mtx.lock();  // 加锁
	std::unique_lock<std::mutex> lg(mtx, std::defer_lock);  // 初始未上锁的mutex
	lg.lock();// 手动上锁
	// ...  // 执行共享数据读写都可以
	//mtx.unlock(); // 解锁  不需要手动解锁
}

void print_try(int i)
{
	std::unique_lock<std::mutex> lg(mtx, std::try_to_lock);  // 尝试上锁
	if (lg.owns_lock)  // 判断是否上锁成功
	{ // 成功
		// ...  // 执行共享数据读写都可以
	}
	else
	{
		std::cout << "获取上锁失败" << std::endl;
		// 做其它事情,不会阻塞
	}
	// 解锁  不需要手动解锁
}

int main()
{
	int a = 8;
	
	std::thread create(print, a);
	create.join();
	
	return 0;
}

【C++11多线程入门教程】系列之unique_lock

4 ===> std::condition_variable

  关于条件变量的出现,主要是为了解决多线程之间的协作问题,消除线程等待时间。最为经典的就是生产消费问题,条件变量主要通过wait()函数来告知另一个线程是否可以去读数据。wait()函数会自动调用lock()unlock()函数来进行加锁与解锁。那么另一个线程通过notify_one()notify_all()函数来告知等待的线程wait()进行唤醒。如此的话,就不会增加多个线程协作之间的等待时间,一旦生产线程完成后,立即解锁唤醒其中一个消费线程读取。

具体事例请参考【C++11多线程入门教程】系列之condition_variable

5 ===> std::asyncstd::future

  函数模板std::async为了异步任务而启用,返回是一个std::future对象。该异步函数std::async并不是立即执行,只有当std::future对象调用get()函数时候,会自动进行阻塞等待std::async执行完毕获取返回对象。但是,关于std::future对象的get()函数只能够调取一次,如果有多次调取需求请参考std::shared_future的调用。

【C++11多线程入门教程】系列之future(一)
【C++11多线程入门教程】系列之future(二)

6 ===> std::atomic

  关于原子类型,这里只强调一句:原子操作是最小的操作,不可分割。同时也不会存在原子类型操作导致的数据访问冲突问题。因此原子类型不需要进行加锁与解锁操作,因为它已经是计算机最小的执行单元。但是,一般原子的操作只有一些简单的操作运算,一般用来计数等。一般支持的运算符操作:++ - += -= &= |= ^=。具体原子操作的细节请参考:

【C++11多线程入门教程】系列之atomic

C++11多线程易错与推荐

  上面对C++11多线程进行简单的知识小结,下面对上述的知识易错点进行总结:

  • 尽量不要使用detach()函数,避免线程未结束而主线程结束造成的异常结果。
  • 使用互斥量mutex时候,一定不要忘记用完解锁unlock(),特别是在作用域复杂时候例如:存在if else等条件判断时候;
  • 当多个线程使用互斥量时候,记得最好有序对不同的互斥量进行加锁or解锁【多个互斥量对象有顺序的加锁与解锁】,避免死锁现象的产生;
  • 简单函数或作用域简单的情况下推荐使用互斥量lock_guard,以此来避免忘记解锁的情况;
  • 当你需要使用更为复杂的多线程之间操作,推荐unique_lock类进行手动控制加锁与解锁时机,但是一定注意不要漏掉解锁;
  • 使用条件变量时候,注意wait()notify_x()函数的使用区间,避免死锁;
  • 使用异步函数模板std::async时候,记得考虑清楚函数入参的不同意义;
  • 类模板std::future接收异步函数std::async时候,并不是立即执行。只有在调用get()函数时候,进行阻塞直到异步函数std::async执行完毕。
  • 类模板std::futureget()函数只能够调用一次,如果有多次需求,使用std::shared_future
  • 原子操作时候,一定要谨记:并不是所有的操作都是原子操作,要确认无误在进行使用;例如:++i是原子操作,但是,i=i+1就不是原子操作。
C++11多线程小结

  上面写了关于C++11多线程小结与避错,总结下来就一句话:严格遵守C++11多线程语法规则进行调用,根据场景应用时候需要时刻考虑会不会造成数据访问冲突问题。如果上面都不存在,那么多线程的编写出错概率将会大大降低。好了,说了这么多。最后,还是那句:如有错误,还请批评指正。

参考

multi-threading
thread
mutex
future
condition_variable
atomic

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值