lesson9: C++多线程

1.线程库

1.1 thread****类的简单介绍

C++11 中引入了对 线程的支持 了,使得 C++ 并行编程时 不需要依赖第三方库
而且在原子操作中还引入了 原子类 的概念。要使用标准库中的线程,必须包含 < thread > 头文件

|-----------------------------------|--------------------------------------------------------------|
| 函数名 | 功能 |
| thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
| thread(fn, args1, args2, ...) | 构造一个线程对象,并关联线程函数fn,args1,args2,...为线程函数的 参数 |
| get_id() | 获取线程id |
| jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程。 |
| jion() | 该函数调用后会 阻塞住线程 ,当该线程结束后,主线程继续执行 |
| detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关 |

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
    状态。
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

1.2 线程对象关联线程函数

cpp 复制代码
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
class TF
{
public:
	void operator()()
	{
		cout << "Thread3" << endl;
	}
};
int main()
{
	// 线程函数为函数指针
	thread t1(ThreadFunc, 10);

	// 线程函数为lambda表达式
	thread t2([](){cout << "Thread2" << endl; });

	// 线程函数为函数对象
	TF tf;
	thread t3(tf);

	t1.join();
	t2.join();
	t3.join();
	cout << "Main thread!" << endl;
	return 0;
}
  • 线程对象可以关联1.函数指针 2.lambda表达式 3.函数对象
  • 当创建一个线程对象后,没有提供线程函数,该对象实际没有 对应任何线程

1.2.1 注意

  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以 移动构造移动赋值 ,即将一个
    线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  2. 可以通过**jionable()**函数判断线程是否是有效的,如果是以下任意情况,则线程无效
    1. 采用无参构造函数构造的线程对象
    2. 线程对象的状态已经转移给其他线程对象
    3. 线程已经调用jion或者detach结束

1.3 线程函数参数

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

void Print(int n, int& x,mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();
		cout <<this_thread::get_id()<<":"<< i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;
		mtx.unlock();
	}

}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count),ref(m));
	thread t2(Print, 10, ref(count),ref(m);

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

	cout << count << endl;

	return 0;
}
  • 线程函数的参数先传递给thread的,并以值拷贝的方式拷贝到线程栈空间中的
  • 如果不给线程函数的参数不借助 ref函数
    • 即使线程参数为 引用类型 ,在线程中修改后也 不能修改外部实参
    • 因为其实际引用的是线程栈中的拷贝,而不是外部实参

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	int m;
	cin >> m;

	vector<thread> v(m);
	//v.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				mtx.lock();

				cout << this_thread::get_id() << ":" << i << endl;
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
				++x;

				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}
  • 借助lambda表达式 中的引用捕捉也可以实现上面那个函数,就可以不用借助ref函数

1.3.1 线程并行 && 并发的讨论

  • 并行:任务的同时进行
  • 并发: 任务的调动和切换
  • 在这个函数中其实是并行的速度更快 ,因为线程切换十分耗时间

1.4 原子性操作库**(atomic)**

多线程最主要的问题是共享数据带来的问题 ( 即线程安全 )
当一个或多个线程要 修改 共享数据时,就会产生很多潜在的麻烦

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	// int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// t1 t2 t3 t4
				++x;
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}
  • C++98中传统的解决方式:可以对共享修改的数据加锁保护
    • 加锁的问题: 这个线程执行的时候, 其他线程就会被阻塞 ,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
  • C++11 中使用atomic类模板,定义出需要的任意原子类型
    • 程序员 不需要 对原子类型变量进行 加锁解锁 操作,线程能够对原子类型变量互斥的访问。

1.4.1 注意

cpp 复制代码
#include <atomic>
int main()
{
     atomic<int> a1(0);
     //atomic<int> a2(a1);   // 编译失败
     atomic<int> a2(0);
     //a2 = a1;               // 编译失败
     return 0;
}
  • 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,
  • 因此在C++11 中,原子类型只能从其模板参数中进行构造****, 不允许原子类型进行拷贝构造、移动构造以及 operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了

1.5 lock_guardunique_lock

多线程 环境下, 原子性 只能保证 某个变量的安全性
多线程环境下,而需要保证一段代码的安全性,就只能通过加锁的方式实现

1.5.1 lock_guard

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

//RAII
template<class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lock(lk)
	{
		_lock.lock();
		cout << "thread:" << this_thread::get_id() << "加锁" << endl;
	}

	~LockGuard()
	{
		cout << "thread:" << this_thread::get_id() << "解锁" << endl << endl;
		_lock.unlock();
	}
private:
	Lock& _lock;// 成员变量是引用
};

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	//int x = 0;
	int n = 100;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				{
					lock_guard<mutex> lk(mtx);
					cout << this_thread::get_id() << ":" << i << endl;
				}

				std::this_thread::sleep_for(std::chrono::milliseconds(100));
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}
  • lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封
  • 调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
  • lock_guard缺陷 :太单一,用户没有办法对该锁进行控制

1.5.2 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所管理的互斥量的指针)。

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

1.6.1 错误案例

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;

	thread t1([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

	this_thread::sleep_for(chrono::microseconds(100));

	thread t2([&](){
		while (i < n)
		{
			mtx.lock();

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;

			mtx.unlock();
		}
	});

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

	return 0;
}
  • 在线程切换的中间时间也会发现线程竞争抢锁的问题

1.6.2 正确案例

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<vector>
#include<atomic>
using namespace std;

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;// 条件变量
	bool ready = true;

	// t1打印奇数
	thread t1([&](){
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready](){return !ready; });// 等待线程

				cout << "t1--" << this_thread::get_id() << ":" << i << endl;
				i += 1;

				ready = true;

				cv.notify_one();// 解除线程等待
			}

			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(100));
		}
	});

	// t2打印偶数
	thread t2([&]() {
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&ready](){return ready; });

			cout <<"t2--"<<this_thread::get_id() << ":" << i << endl;
			i += 1;
			ready = false;

			cv.notify_one();
		}
	});

	this_thread::sleep_for(chrono::seconds(3));

	cout << "t1:" << t1.get_id() << endl;
	cout << "t2:" << t2.get_id() << endl;

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

	return 0;
}

  • cv.wait(lock, [&ready]() {return !ready; });
    • ready返回的是false时,这个线程就会阻塞
    • 阻塞当前线程,并自动调用lock.unlock(),允许其他锁定的线程继续执行
  • cv.notify_one();
    • 唤醒当前线程并自动调用l**ock.lock();**就只允许自己一个线程执行

1.7 shared_ptr的多线程问题

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
#include<atomic>
#include<memory>
using namespace std;

namespace bit
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			, _pMutex(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			, _pMutex(sp._pMutex)
		{
			AddRef();
		}

		void Release()
		{
			bool flag = false;

			_pMutex->lock();
			if (--(*_pRefCount) == 0 && _ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pRefCount;

				flag = true;
			}
			_pMutex->unlock();

			if (flag)
				delete _pMutex;
		}

		void AddRef()
		{
			_pMutex->lock();

			++(*_pRefCount);

			_pMutex->unlock();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pMutex = sp._pMutex;
				AddRef();
			}

			return *this;
		}

		int use_count()
		{
			return *_pRefCount;
		}

		~shared_ptr()
		{
			Release();
		}

		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pRefCount;// 使用时需要加锁
		mutex* _pMutex;// 锁指针
	};
}

int main()
{
	// shared_ptr是线程安全的吗?
	bit::shared_ptr<double> sp1(new double(1.11));
	bit::shared_ptr<double> sp2(sp1);

	mutex mtx;

	vector<thread> v(2);
	int n = 100000;
	for (auto& t : v)
	{
		t = thread([&](){
			for (size_t i = 0; i < n; ++i)
			{
				// 拷贝是线程安全的
				bit::shared_ptr<double> sp(sp1);

				// 访问资源不是
				mtx.lock();
				(*sp)++;
				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << sp1.use_count() << endl;
	cout << *sp1 << endl;

	return 0;
}
  • 在多线程中,shared_ptr也应该对自己的引用计数进行加锁处理

  • 在多线程中, shared_ptr的拷贝是线程安全的,但访问资源不是,所以访问资源也需要加锁

1.8 单例模式的多线程问题

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 保护第一次,后续不需要加锁
		// 双检查加锁
		if (_pInstance == nullptr)
		{
			unique_lock<mutex> lock(_mtx);
			if (_pInstance == nullptr)
			{
				_pInstance = new Singleton;
			}
		}

		return _pInstance;
	}

private:
	// 构造函数私有
	Singleton(){};

	// C++11
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;

	static Singleton* _pInstance;
	static mutex _mtx;
};

Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx; 

int main()
{
	Singleton::GetInstance();
	Singleton::GetInstance();

	return 0;
}
  • 在多线程的情况下, 第一次创建对象时也是需要加锁保护

1.8.1 巧妙的解决方案

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		static Singleton _s;// 局部的静态对象,第一次调用时初始化

		return &_s;
	}

private:
	// 构造函数私有
	Singleton() {};

	// C++11
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};

int main()
{
	Singleton::GetInstance();
	Singleton::GetInstance();

	return 0;
}
  • 局部的静态对象,第一次调用时初始化
  • 在C++11之前是不能保证线程安全的
    静态对象的构造函数调用初始化并不能保证线程安全的原子性
  • C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上玩
相关推荐
NEFU AB-IN43 分钟前
Prompt Gen Desktop 管理和迭代你的 Prompt!
java·jvm·prompt
唐古乌梁海6 小时前
【Java】JVM 内存区域划分
java·开发语言·jvm
众俗7 小时前
JVM整理
jvm
echoyu.7 小时前
java源代码、字节码、jvm、jit、aot的关系
java·开发语言·jvm·八股
代码栈上的思考1 天前
JVM中内存管理的策略
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之进阶部分
jvm
沐浴露z1 天前
【JVM】详解 线程与协程
java·jvm
thginWalker1 天前
深入浅出 Java 虚拟机之实战部分
jvm
程序员卷卷狗3 天前
JVM 调优实战:从线上问题复盘到精细化内存治理
java·开发语言·jvm
Sincerelyplz3 天前
【JDK新特性】分代ZGC到底做了哪些优化?
java·jvm·后端