首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++11第五弹:线程库 | 互斥锁 | 原子操作

C++11第五弹:线程库 | 互斥锁 | 原子操作

作者头像
南桥
发布于 2024-09-20 08:30:33
发布于 2024-09-20 08:30:33
4530
举报
文章被收录于专栏:南桥谈编程南桥谈编程

thread类的简单介绍

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

线程库文档

函数名

功能

thread()

构造一个线程对象,没有关联任何线程函数,即没有启动任何线程

thread(fn,args1, args2,…)

构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数参数导管

get_id()

获取线程id

jionable()

线程是否还在执行,joinable代表的是一个正在执行中的线程

jion()

该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行

detach()

在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

线程函数为函数指针

代码语言:javascript
复制
void Printf(int n,int i)
{
	for (; i < n; i++)
	{
		cout << i << endl;
	}
	cout << endl;
}

int main()
{
	thread t1(Printf, 100, 0);
	thread t2(Printf, 200, 100);
	
	cout << t1.get_id() << endl;

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

	cout << this_thread::get_id() << endl;
	return 0;
}

上述代码实现两个线程并发打印。

对于访问同一个变量,会出现线程安全问题,因此需要加锁,加锁需要包含一个<mutex>头文件。对于线程的互斥,在线程ID与互斥中有详细解释,本文不再细谈。

代码语言:javascript
复制
int x;
mutex mtx;

void Printf(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		x++;
	}
	mtx.unlock();
}

int main()
{
	thread t1(Printf, 10000);
	thread t2(Printf, 20000);

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

	cout << x << endl;

	return 0;
}

线程中传参问题:多个线程访问和修改同一个 x 变量,并使用相同的 mutex 对象来确保线程安全。std::ref 使得 x 和 mtx 被作为引用传递,这样线程函数 Printf 就能直接修改主线程中的 x 和 mtx。

代码语言:javascript
复制
void Printf(int n, int& rx, mutex& rmtx)
{
	rmtx.lock();
	for (int i = 0; i < n; i++)
	{
		rx++;
	}
	rmtx.unlock();
}

int main()
{
	int x = 0;
	mutex mtx;

	thread t1(Printf, 10000, ref(x), ref(mtx));
	thread t2(Printf, 20000, ref(x), ref(mtx));



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

	cout << x << endl;

	return 0;
}

线程函数为lambda表达式

[&] 是 lambda 表达式的捕获列表,表示 lambda 函数可以捕获外部作用域的所有变量的引用。这意味着在 lambda 表达式中可以直接访问和修改主线程中的变量,而不需要将它们作为参数传递。

代码语言:javascript
复制
int main()
{
	int x = 0;
	mutex mtx;

	thread t1([&] {
		mtx.lock();
		for (int i = 0; i < 10000; i++)
		{
			++x;
		}
		mtx.unlock();
		});
	thread t2([&] {
		mtx.lock();
		for (int i = 0; i < 20000; i++)
		{
			++x;
		}
		mtx.unlock();
		});

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

	cout << x << endl;

	return 0;
}

创建多个线程

代码语言:javascript
复制
int main()
{
	vector<thread> vthd;
	int n;
	cin >> n;
	vthd.resize(n);

	int x = 0;
	mutex mtx;
	auto func = [&](int n) {
		mtx.lock();
		for (size_t i = 0; i < n; i++)
		{
			++x;
		}
		mtx.unlock();
	};

	for (auto& thd : vthd)
	{
		//移动赋值
		thd = thread(func, 10000);
	}

	for (auto& thd : vthd)
	{
		thd.join();
	}

	cout << x << endl;

	return 0;
}

互斥锁

互斥锁文档

lock_guard

在使用lock和unlock时,如果中间抛异常,那么就无法解锁,就是死锁。因此设计了rna的概念,有了lockguard函数。

在库中的lock_guard仅支持构造和析构函数。

模拟实现lockguard:

代码语言:javascript
复制
class LockGuard
{
public:
	LockGuard(mutex& mtx)
		:_mtx(mtx)
	{
		_mtx.lock();
	}

	~LockGuard()
	{
		_mtx.unlock();
	}

private:
	mutex& _mtx;
};

LockGuard lock(mtx)构造一个LockGuard的对象,会有一个引用将mutex保存起来,获取锁,将锁锁住,除了作用域后,会调用析构函数,解锁。无论是正常结束还是抛异常,生命周期都会结束,都会调用析构函数。

如果有两个循环,只想锁住第一个循环,不锁第二个循环。在使用lock和unlock时可以控制范围,那么LockGuard如何解决?

解决方法:增加一个局部域,可以显示控制对象的生命周期:

unique_lock

支持手动加锁解锁,相比于lock_guard功能更丰富些。

原子操作

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

atomic文档

atomic是一个类,支持原子的加减或者异或,在使用时需要<atomic>头文件

所支持的类型:

代码语言:javascript
复制
class LockGuard
{
public:
	LockGuard(mutex& mtx)
		:_mtx(mtx)
	{
		_mtx.lock();
	}

	~LockGuard()
	{
		_mtx.unlock();
	}

private:
	mutex& _mtx;
};

int main()
{
	vector<thread> vthd;
	int n;
	cin >> n;
	vthd.resize(n);

	//atomic<int> x = 0;
	atomic<int> x{ 0 };
	mutex mtx;
	auto func = [&](int n) {
		//mtx.lock();
		{
			LockGuard lock(mtx);
			for (size_t i = 0; i < n; i++)
			{
				++x;
			}
			//mtx.unlock();
		}
	};

	for (auto& thd : vthd)
	{
		//移动赋值
		thd = thread(func, 10000);
	}

	for (auto& thd : vthd)
	{
		thd.join();
	}

	cout << x << endl;

	return 0;
}

条件变量

阅读文档

条件变量(std::condition_variable)是用于线程间同步的工具,它可以使一个线程等待某个条件的发生,而另一个线程则在条件发生时通知等待的线程。条件变量通常与互斥锁(std::mutex)一起使用,以确保线程在等待或通知条件时不会引发数据竞争。

condition_variable和Linux posix的条件变量并没有什么大的区别,主要还是面向对象实现的。

Linux的条件变量可阅读博客:线程同步-条件变量

两个线程交替打印,一个打印奇数,一个打印偶数

代码语言:javascript
复制
void two_thread_print()
{
	std::mutex mtx;
	condition_variable c;
	int n = 100;
	bool flag = true;

	thread t1([&]() {
		int i = 0;
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			c.wait(lock, [&]()->bool {return flag; });
			cout << i << endl;
			flag = false;
			i += 2; // 偶数
			c.notify_one();
		}
		});

	thread t2([&]() {
		int j = 1;
		while (j < n)
		{
			unique_lock<mutex> lock(mtx);
			c.wait(lock, [&]()->bool {return !flag; });
			cout << j << endl;
			j += 2; // 奇数
			flag = true;
			c.notify_one();
		}
		});

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

int main()
{
	two_thread_print();
	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-16,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • thread类的简单介绍
    • 线程函数为函数指针
    • 线程函数为lambda表达式
    • 创建多个线程
  • 互斥锁
    • lock_guard
    • unique_lock
  • 原子操作
  • 条件变量
    • 两个线程交替打印,一个打印奇数,一个打印偶数
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档