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以后的编译器上玩
相关推荐
Ysjt | 深3 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
阿伟*rui11 小时前
jvm入门
jvm
学点东西吧.14 小时前
JVM(五、垃圾回收器)
jvm
请你打开电视看看16 小时前
Jvm知识点
jvm
程序猿进阶17 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
阿龟在奔跑1 天前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
王佑辉1 天前
【jvm】方法区常用参数有哪些
jvm
王佑辉1 天前
【jvm】HotSpot中方法区的演进
jvm
Domain-zhuo1 天前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
Theodore_10222 天前
7 设计模式原则之合成复用原则
java·开发语言·jvm·设计模式·java-ee·合成复用原则