【C++】多线程相关

发布时间:2023年12月18日

多线程的问题,在C++11之前,都是和平台相关联的。比如Windows和Linux平台各有自己的一套多线程的接口,这就使得多线程编程相关的代码的可移植性比较差。而从C++11开始,引入了线程相关的库,使得C++多线程编程时不再需要接入第三方的库。
在这里插入图片描述

线程控制

  1. 线程对象的构造:
    在这里插入图片描述
    线程本质上还是操作系统层次的概念,C++11创建的线程对象可以通过关联一个线程,从而达到控制线程以及获取线程状态的目的。
    thread():创建线程对象后,因为没有提供线程的执行函数,所以实际上没有关联任何线程;
    thread(Fn&& fn, Args&&... args):创建线程对象时,提供了线程执行函数Fn,和函数的一系列参数args
    thread(const thread&) = delete&thread(thread&& x):线程对象是防拷贝的,不能拷贝构造以及赋值,但提供了移动构造以及移动赋值。也就是说可以将线程对象关联线程的状态转移给其它线程对象,转移期间不影响线程执行。
    在创建线程对象时,关联线程执行函数,线程就将启动,和主线程一同运行。而线程执行函数可以通过函数指针、仿函数、lambda表达式的方式进行传入。

  2. join线程:
    在这里插入图片描述
    join:会阻塞式等待线程退出;
    joinable:可以判断线程是否有效,以下情况之一无效:
    在这里插入图片描述

  3. detach线程:
    在这里插入图片描述
    用于把被创建的线程与线程对象分离开,分离的线程变为后台线程,线程执行结束会自动销毁。

线程执行函数的参数问题

线程执行函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此即使形参使用引用类型,其修改也不能影响外部实参。
但可以使用ref()强制进行引用。

void Print2N(int n, int& count)
{
	for (int i = 0; i < n; ++i)
		++count;
}
void test1()
{
	int count = 0;
	//thread t1(Print2N, 10, count);
	thread t1(Print2N, 10, ref(count));

	t1.join();

	cout << count << endl;
}

如果是类成员函数作为参数传入,必须同时将this指针作为参数传入。

mutex

C++11中,一共包含了四种互斥量锁。
在这里插入图片描述

  1. mutex
    mutex:是C++11提供的最基本的互斥量。
    mutex常用的三个接口如下:
    在这里插入图片描述

lock()时,可能会发生以下三种情况:

  • 如果互斥量当前没有被锁住,则线程lock()会将互斥量锁住,在调用unlock()之前,该线程一直占有该锁;
  • 如果互斥量当前被其它线程锁住,则当前线程lock()会被阻塞;
  • 如果当前线程已经锁住互斥量,在未unlock()之前,再次lock()会产生死锁(deadlock)。

try_lock()时,可能会发生以下三种情况:

  • 如果互斥量当前没有被锁住,则线程try_lock()会将互斥量锁住,在调用unlock()之前,该线程一直占有该锁;
  • 如果互斥量当前被其它线程锁住,则当前线程try_lock()会失败,返回false,但并不会阻塞
  • 如果当前线程已经锁住互斥量,在未unlock()之前,再次try_lock()会产生死锁(deadlock)。
  1. recursive_mutex
    recursive_mutex(递归锁),允许同一个线程对互斥量多次上锁(即递归上锁),来获取对互斥量对象的多层所有权;但相应的,释放锁时,需要调用相同层次深度的unlock()

  2. timed_mutex
    timed_mutexmutex多了两个成员函数:
    在这里插入图片描述
    这两个函数的使用可以做到见名知意。同时第四种锁recursive_timed_mutex的使用也应该明了了。

void test2()
{
	mutex mtx;
	vector<thread> threads(4);

	for (size_t i = 0; i < threads.size(); ++i)
	{
		// 线程执行函数通过lambda传入;构造匿名的线程对象,移动赋值给vector中的线程对象
		threads[i] = thread([&mtx]() {
			mtx.lock();

			for (int i = 0; i < 10; ++i)
			{
				cout << this_thread::get_id() << ": " << i << endl;
				this_thread::sleep_for(chrono::milliseconds(100)); // 线程 sleep 100毫秒
			}

			// 一个线程在没有从0到N打印完之前不会释放锁
			mtx.unlock();
		});
	}

	for (auto& t : threads)
		t.join();
}

RAII 管理锁

在这里插入图片描述

lock_guard

lock_guard主要通过RAII的方式,对其管理的互斥量进行了封装。在需要加锁的地方,只需实例化一个lock_guard对象,调用构造函数即可上锁;出作用域时,lock_guard对象被销毁,调用析构函数即可自动解锁。这样的处理方式可以有效地避免一些死锁的问题。

// lock_guard 的模拟实现
template<class lk>
class lockGuard
{
public:
	lockGuard(lk& lock)
		: _lock(lock) { _lock.lock();}
	~lockGuard() { _lock.unlock();}
private:
	lk& _lock;
};
void test3()
{
	mutex mtx;
	vector<thread> threads(4);

	for (size_t i = 0; i < threads.size(); ++i)
	{
		threads[i] = thread([&mtx]() {
			// 如果中间抛异常就会出现死锁
			// 解法1: try-catch
			// 解法2: RAII - lock_guard
			lock_guard<mutex> lg(mtx);
			for (int i = 0; i < 10; ++i)
			{
				cout << this_thread::get_id() << ": " << i << endl;
				this_thread::sleep_for(chrono::milliseconds(100));
			}
		});
	}

	for (auto& t : threads)
		t.join();
}

unique_lock

lock_guard可以理解为是全自动的,把互斥量交给lock_guard后,用户就没有办法再通过lock_guard对象对互斥量进行操控,为了弥补这一点,C++11又提供了unique_lock
unique_locklock_guard更加灵活,提供了更多的构造方式和功能操作。
在这里插入图片描述

  • 上锁/解锁操作:locktry_locktry_lock_fortry_lock_untilunlock
  • 修改操作:operator=(移动赋值),swap(与另一个unique_lock对象交换它们所管理的mutex对象的所有权),release(返回所管理mutex对象的指针,并释放所有权)
  • 获取属性操作:owns_lock(返回当前unique_lock对象是否获得了锁),operator bool(与owns_lock功能相同),mutex(返回unique_lock对象所管理的mutex对象的指针)

原子操作

加锁操作也是有缺陷的:大量的加锁解锁可能会影响到程序效率;一个线程加锁访问数据,其它线程只能在锁外阻塞,锁的使用如果控制不好,还会造成死锁等问题。
而引入原子操作(不可中断的一个或一系列操作),可以使得线程不在像加锁解锁那样繁重,对数据的访问更加高效。

void test4()
{
	atomic<int> count = 0;
	vector<thread> threads(4);

	for (size_t i = 0; i < threads.size(); ++i)
	{
		threads[i] = thread([&count]() {
			for (int i = 0; i < 100000; ++i)
			{
				++count; // 原子的++操作
			}
		});
	}

	for (auto& t : threads)
		t.join();

	cout << count << endl;
}

不需要对原子类型变量进行加锁解锁操作,县城也能够对原子类型变量互斥地访问。

两个线程交替打印奇偶数

交替打印,需要进行线程间运行的同步操作,可以使用条件变量进行同步操作。

void test5()
{
	mutex mtx;
	condition_variable cv;
	bool ready = true;

	thread t1([&mtx, &cv, &ready]() {
		for (int i = 1; i <= 100; i += 2)
		{
			unique_lock<mutex> u_lock(mtx);
			cv.wait(u_lock, [&ready]() {return ready; });
			cout << "t1: " << i << endl;
			ready = !ready;
			cv.notify_one();
		}
		});

	thread t2([&mtx, &cv, &ready]() {
		for (int i = 2; i <= 100; i += 2)
		{
			unique_lock<mutex> u_lock(mtx);
			cv.wait(u_lock, [&ready]() {return !ready; });
			cout << "t2: " << i << endl;
			ready = !ready;
			cv.notify_one();
		}
		});

	t1.join();
	t2.join();
}
文章来源:https://blog.csdn.net/weixin_62172209/article/details/134935531
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。