前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【C++】C++11之线程库

【C++】C++11之线程库

作者头像
青衫哥
发布2023-10-17 08:45:32
4060
发布2023-10-17 08:45:32
举报
文章被收录于专栏:C++打怪之路
一、thread类

在 C++11 之前,涉及到多线程问题,都是和平台相关的,比如 windows linux 下各有自己的接 口,这使得代码的可移植性比较差C++11 中最重要的特性就是对线程进行支持了,使得 C++ 并行编程时不需要依赖第三方库 ,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread > 头文件。

我们可以参考下面文档:C++ thread类

1.1 thread类的构造方法

1、支持无参构造。构造一个空线程对象,由于没有关联的线程函数,所以不会直接运行。

2、支持可变参数构造。(最常用) 构造一个线程对象,并关联线程函数fun,args1,args2,...为线程函数的参数。

代码语言:javascript
复制
#include<iostream>
#include<thread>

using namespace std;

void Add(int x, int y)
{
	cout << x + y << endl;
}

int main()
{
	int a = 10, b = 30;
	thread t1(Add, a, b);
	t1.join();
	return 0;
}

这里join函数的作用是让线程运行完进程进行回收。不然就会造成资源不回收,引发内存泄漏。 

3、不支持拷贝构造。

4、支持移动赋值。

1.2 其他函数接口

get_id:获取线程id,也是线程的唯一标识。get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:

代码语言:javascript
复制
typedef struct
{     /* thread identifier for Win32 */
     void *_Hnd; /* Win32 HANDLE */
     unsigned int _Id;
} _Thrd_imp_t;

join:等待线程回收分配给线程的资源。

joinable:用于判断是否需要回收线程资源。                  

detach:线程与主线程分离,彼此独立运行。两个线程继续,不会以任何方式阻塞或同步。请注意,当任何一个结束执行时,都会释放其自己的资源。

注意

1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的状态

2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。

线程函数一般情况下可按照以下三种方式提供:

  • 函数指针
  • lambda表达式
  • 函数对象(仿函数)
代码语言:javascript
复制
class temp
{
public:
	void operator()()
	{
		cout << "thread t3" << endl;
	}
};

int main()
{
	thread t1(Add, 1, 2);
	thread t2([]() {cout << "thread t2" << endl; });
	temp t;
	thread t3(t);

	t1.join();
	t2.join();
	t3.join();
	return 0;
}

4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。

5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

2.3 this_thread命名空间

 get_id:用于获取线程id。

sleep_for:进程睡眠一段时间。

sleep_until:进程睡眠至某个时间。

由于不会特别常用,这里就不详细介绍,需要用时差文档即可:this_thread - C++ Reference (cplusplus.com)


二、mutex锁

2.1 mutex类

多线程最主要的问题是共享数据带来的问题(即线程安全)。 如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦

比如:

我们在实现++操作的时候,看起来是一行代码,实际上底层汇编有三条。当我们进行到一半而时间片的时间到了,那么该线程就会被切走阻塞,让别的线程来使用cpu,而如果后来的线程也对a进行++操作,操作后再把原来的进程切换回来,原来的进程操作的还是原来的a,那么最后的结果就会出现问题。

案例代码:

代码语言:javascript
复制
int ret = 0;

void Func()
{ 
	int n = 10000;
	while (n--)
	{
		ret++;
	}
}

int main()
{
	thread t1(Func);
	thread t2(Func);

	t1.join();
	t2.join();

	cout << ret << endl;

	return 0;
}

 为了解决这个问题,引入了锁mutex来使得++操作一次完成。

 mutex类用到的主要两个函数就是:lock 和 unlock

代码语言:javascript
复制
mutex m;
int ret = 0;

void Func()
{ 
	int n = 10000;
	while (n--)
	{
		m.lock();
		ret++;
		m.unlock();
	}
}

int main()
{
	thread t1(Func);
	thread t2(Func);

	t1.join();
	t2.join();

	cout << ret << endl;

	return 0;
}

2.2 recursive_mutex

mutex类的锁是不能够递归加锁的,会出问题。为了适应这种情况,引入了recursive_mutex类。

 该类提供的函数接口和mutex类一样,但是允许一个线程多次加锁,来获得互斥对象的多个级别的所有权。

2.3 timed_mutex

相较于上面两种锁,timed_mutex锁增加了两个功能:try_lock_for 和 try_lock_until 

try_lock:能够在一定的时间范围内申请锁。如果当前锁未被申请,那么调用的线程就将取走锁;如果当前锁已经被申请了,那么就会返回false。

try_lock_until:尝试申请锁知道某个时间点。


三、原子性操作库(atomic)

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对 sum++ 时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作, C++11 引入的原子操作类型,使得线程间数据的同步变得非常高效。

该类的使用需要包含头文件<atomic>

我们下面看atomic类的构造方法:

可以看到:支持无参构造和列表初始化,但是不能拷贝。

原子类型通常属于 " 资源型 " 数据,多个线程只能访问单个原子类型的拷贝,因此 C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator= 等,为了防止意外,标准库已经将 atmoic 模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。

使用案例:

代码语言:javascript
复制
atomic<int> a;
void func()
{
	int n = 10000;
	while (n--)
	{
		a++;
	}
}

int main()
{
	thread t1(func);
	thread t2(func);

	t1.join();
	t2.join();

	cout << a << endl;

	return 0;
}

在 C++11 中, 程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的 访问 。更为普遍的,程序员可以使用 atomic 类模板,定义出需要的任意原子类型


四、利用RAII机制管理锁

4.1 lock_guard

这是一个C++中定义的用来管理锁的类,在构造对象时候加锁,析构对象的时候解锁。

实现代码:

代码语言:javascript
复制
template<class _Mutex>
class lock_guard
{
public:
	explicit lock_guard(_Mutex& _Mtx)
		:_MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}

	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		:_MyMutex(_Mtx)
	{}

	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}

	lock_guard(const lock_guard&) = delete;
	lock_guard& operato = (const lock_guard&) = delete;
private:
	_Mutex _MyMutex;
};

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

案例:

代码语言:javascript
复制
int a = 0;
mutex mx;

void func()
{
	int n = 10000;
	lock_guard<mutex> mt(mx);
	while (n--)
	{
		a++;
	}
}

int main()
{
	thread t1(func);
	thread t2(func);

	t1.join();
	t2.join();

	cout << a << endl;

	return 0;
}

4.2 unique_lock

与 lock_gard 类似, unique_lock 类模板也是采用 RAII 的方式对锁进行了封装,并且也是以独占所 有权的方式管理 mutex 对象的上锁和解锁操作,即其对象之间不能发生拷贝 。在构造 ( 或移动(move)赋值 ) 时, unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。 使用以上类型互斥量实例化 unique_lock 的对象时,自动调用构造函数上锁, unique_lock 对象销毁时自动调用析构函数解 锁,可以很方便的防止死锁问题。

与 lock_guard 不同的是, unique_lock 更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

五、条件变量

我们先看一道题:两个线程交替打印0-100的数字,一个打印奇数,一个打印偶数。

我们通常情况下解题是while循环中用if条件判断来判断,一个线程t1判断奇数打印,一个线程t2判断偶数打印,然后打印完++。但是当我们打印t1奇数的时候,此时时间片切到t2,t2会不断的循环判断,直到时间片切回t1。这样就造成了CPU资源的浪费。

这里就要引入我们的条件变量:std::condition_variable、

条件变量中的 wait 和 notify_one 的接口能够实现进程的等待和唤醒。 使得进程避免因为不满足条件而一直循环判断,浪费资源。

 需要注意的是:

wait接口的参数是unique_lock类型。 有人会好奇为什么需要传一个锁进来呢? 因为条件变量操作不是原子性的,我们需要加锁保护,但是我们加了锁让线程等待,但是其他线程因为申请不到锁也会进入阻塞,那么不就死循环了吗? 其实并不是的,wait操作之所以需要传一个锁进来,就是因为wait操作的同时,会将锁释放,让其他线程能够申请到锁,直到用notify_one来唤醒线程的时候,才会重新持有锁。

有了条件变量,我们可以让进程在不满足条件的时候进行等待,在满足条件之后再唤醒进程运行。

案例代码:

代码语言:javascript
复制
int main()
{
	int a = 0;
	condition_variable cv;
	mutex mt;
	//打印奇数
	thread t1([&]()
		{
			while (a <= 100)
			{
				unique_lock<mutex> lock(mt);
				if (a % 2 == 0)
				{
					cv.wait(lock);
				}
				cout << "t1->" << a << endl;
				++a;

				cv.notify_one();
			}
		});

	thread t2([&]()
		{
			while (a <= 100)
			{
				unique_lock<mutex> lock(mt);
				if (a % 2 == 1)
				{
					cv.wait(lock);
				}
				cout << "t2->" << a << endl;
				++a;

				cv.notify_one();
			}
		});

	t1.join();
	t2.join();
	return 0;
}

我们这里就发现问题了,怎么会打印出101来呢?

原因出在这里:

 因此我们只需要把t1时的循环条件<= 改成 < 即可,这样,在100的时候进不去循环了,自然后面的操作也就不会执行了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 thread类的构造方法
  • 1.2 其他函数接口
  • 注意
  • 2.3 this_thread命名空间
  • 二、mutex锁
    • 2.1 mutex类
      • 2.2 recursive_mutex
        • 2.3 timed_mutex
        • 三、原子性操作库(atomic)
        • 四、利用RAII机制管理锁
          • 4.1 lock_guard
            • 4.2 unique_lock
            • 五、条件变量
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档