C++多线程编程

内容更新

/*

	基本的创建和使用线程方法:
		1. thread mythread(func);		// 创建一个线程,并执行 func 函数
		2. thread mythread(func, a, b);	// 将参数赋值一份,然后再传递(即:按照所谓的值传递进行)
		3. mythread.join();				// join: 加入,汇聚; 这里是等待其他进程的汇聚
		4. mythread.detach();			// detach: 分离;	这里是与主线程分离的意思,脱离主线程,它运行解释后,由系统自动回收资源
		5. mythread.joinable();			// 用来判断是否可以成功使用 join() 或 detach(),返回 true 或 false

	其他创建线程的方法:
		1. 仿函数
		2. 用 lambda 表达式

	常见陷阱:
		1. 如果要在多线程中使用 detach(),在传递参数的过程中,不要使用指针和引用!
			thread mythread(func, 不要使用指针, 不要使用引用)
		2. 如果要在多线程中使用 detach(),在传递参数的过程中,请使用临时构造的对象作为参数,这样可以复制对象
			char mystr[] = "hello world";
			thread mythread(func, a, b, string(char) );
		3. 建议不适应 detach(); 只使用 join(); 这样就不存在上面的问题————局部变量失效导致线程对内存的非法引用问题

	输出线程id:
		cout << this_thread::get_id() << endl;

	创建多个线程
		for(...)

	数据共享问题:
		1. 只读数据,是安全稳定的,不需要特别的处理手段,直接读就可以
		2. 有读有写,有的线程在写,有的线程在读,如果没有特别的处理,程序一定崩溃
		解决办法:使用互斥量:

			(1)基本使用:
				#include<mutex>		// 引入头文件
				mutex my_mutex;		// 创建互斥量
				my_mutex.lock();	// 上锁
				...					// 要被保护的代码
				my_mutex.unlock();	// 开锁

			(2)lock() 和 unlock() 一定要成对使用
				为了防止大家忘记 unlock(),C++引入了一个叫 std::lock_guard 的类模板,它类似于智能指针,能够自动帮你 unlock()

			(3)std::lock_guard 类模板:直接取代 lock() 和 unlock()

				int func()
				{
					std::lock_guard<std::mutex> myGuard(my_mutex);
					// 它的原理是:
					// 在创建对象的时候,构造函数里面执行了 mutex::lock();
					// 在销毁对象的时候,析构函数里面执行了 mutex::unlock();
					// 优点:不用担心忘记开锁
					// 缺点:不方便指定特定部分进行上锁和开锁,虽然不方便,但是如果需要提前开锁,可以使用{}符号创建一个代码块,从而产生一个匿名的作用域
					// 但是,当 std::lock_guard 对象离开其作用域(即代码块结束时)时,它会自动解锁互斥锁
					...
					...
					return 1;
				}

	死锁问题:
		1. 一般的解决办法:保证这两个互斥量上锁的顺序一致就不会死锁

		2. 使用 std::lock() 函数模板————它能够一次锁住两个或两个以上的互斥量
		工作原理:
			现有两个互斥量:mutex1 和 mutex2
			std::lock() 会尝试上锁 mutex1,如果上锁成功后,继续尝试上锁 mutex2
			如果上锁 mutex2 失败了,它会放弃上锁 mutex2,同时解锁 mutex1,等待一段时间后,再次尝试刚才的上锁步骤
			虽然使用 std::lock(mutex1, mutex2) 上锁很方便,但是使用它还是需要"记得" mutex1.unlock() 和 mutex2.unlock()
			如果忘记解锁,程序也是不正常的

		3. 使用 std::lock 和 std::lock_guard 搭配
		例子:
			std::lock(mutex1, mutex2);		// 相当于每个互斥量都调用了.lock();
			std::lock_guard<std::mutex> myGuard(mutex1, std::adopt_lock);
			std::lock_guard<std::mutex> myGuard(mutex2, std::adopt_lock);
			...		// 已经成功上锁,而且也会自动解锁,这里正常写代码即可
			...		// 同上,这里正常写代码即可
		原理:
			std::lock_guard 的 std::adopt_lock 参数是一个结构体对象,起到一个标记的作用
			即:标记这个互斥量已经lock()过了,不需要在构造函数里面再lock一次


	unique_lock:
		unique_lock 是一个类模板,工作中一般推荐使用 lock_guard
		unique_lock 比 lock_guard 灵活很多,但是也有缺点:效率差一点,内存占用多一点


	unique_lock 的参数:

		(1)std::adopt_lock:

			std::lock(mutex1, mutex2);		// 相当于每个互斥量都调用了.lock();
			std::unique_lock<std::mutex> myLock(mutex1, std::adopt_lock);	// 这里的参数含义和上面相同
			std::unique_lock<std::mutex> myLock(mutex2, std::adopt_lock);	// 这里的参数含义和上面相同

		(2)std::try_to_lock:
			// 它会尝试调用 mutex 的 lock() 去上锁这个 mutex,但是如果上锁失败,它会立即返回,不会阻塞在那里
			// 注意,使用这个参数的时候,不能自己先执行 lock()

			std::unique_lock<std::mutex> myLock(mutex1, std::try_to_lock);	// 尝试上锁
			if (myLock.owns_lock())
			{
				// 已成功上锁
			}
			else
			{
				// 尝试上锁失败,干点别的事情...
			}

		(3)std::defer_lock

			// 它的意思是"延迟加锁",即初始化了一个没有给 mutex 加锁的 unique_lock 对象
			// 初始化这个对象,是为了后面使用它的成员函数
			// 注意:使用这个参数的时候,不能自己先执行 lock()
			std::unique_lock<std::mutex> myLock(mutex1, std::defer_lock);	// 只初始化,不会加锁和自动解锁
			myLock.lock();		// 调用对象的 lock() 功能,它的优点是:上锁了之后,它自动会解锁,不需要我们自己手动解锁
			myLock.unlock();	// 虽然已经有了自动解锁,但是还是有必要提供 unlock() 方法,这样做的目的是:有时候我们需要提前解锁,提供手动 unlock() 的方法能够让我们解锁更灵活
			myLock.try_lock();	// 尝试给互斥量进行加锁,如果加锁失败,返回 false,如果加锁成功,返回 true,这个函数是不阻塞的
			std::mutex *ptr = myLock.release();	// 返回它所管理的 mutex 对象指针,并释放所有权;也就是说,这个 unique_lock 和 mutex 不再有关系

	unique_lock 所有权的传递:

		std::unique_lock<std::mutex> myLock(mutex1);	// myLock 拥有 mutex1 的所有权
		std::unique_lock<std::mutex> yourLock(std::move(myLock))	// 移动语义,现在相当于 myLock 对 mutex1 的所有权转让给了 yourLock

	std::call_once();
		基本介绍:
		// C++11 引入的函数,该函数的第二个参数是一个函数名func
		// 它的作用:能够保障func函数只被调用一次
		// 所以,它具备互斥量的能力;而且在效率上,比互斥量消耗的资源更少
		// 使用上述功能的前提:需要结合一个标记来使用,这个标记是 std::once_flag ———— 它本质是一个结构
		// call_once() 就是通过这个标记来判断对应的函数func()是否被调用
		// 调用 call_once() 成功后,它就把 std::once_flag 这个标记设置为一种已调用的状态

		使用如下:
		// std::once_flag myFlag;
		// std::call_once(myFlag, func);


	std::condition_variable(条件变量)
		作用:当某个条件不满足时,线程将会阻塞,而当条件满足了,又能唤醒被阻塞的线程
		常用的成员函数:
			wait()			// 等待
			notify_one()	// 通知某个
			notify_all()	// 通知所有
		详细学习:https://www.bilibili.com/video/BV1xU421Z7MR/


	std::async 和 std::future
		// 作用:启动一个异步任务
		// 使用方法:

			int func()
			{
				...
			}

			std::future<int> result = std::async(func);

			// 枚举类型:
			std::future_status status = result.wait_for(std::chrono::seconds);	// 等待线程1秒钟,如果线程运行时间超过1秒,我就不等了
			if (status == std::future_status::timeout)
			{
				// 说明当前等待超时,线程执行太久了
			}
			else if (status == std::future_status::ready)
			{
				// 表示线程执行完毕,线程成功返回
				// 如果使用get()就直接可以获取结果
				cout << result.get() << endl;
			}
			cout << result.get() << endl;	// 如果拿不到数据就卡在这里,誓不罢休

		// std::async 更多内容:https://www.cnblogs.com/chengyuanchun/p/5394843.html
		// std::future 更多内容:https://blog.csdn.net/c_base_jin/article/details/89761718


	std::packaged_task
		作用:打包任务,把任务包装起来
		它是一个类模板,它的模板参数是各种可调用对象,通过 std::packaged_task 来把各种可调用对象包装起来,方便将来作为线程入口函数调用


	std::promise
		作用:我们能够在某个线程中给std::promise对象赋值,然后我们可以再其他线程中,把这个值取出来用
		它是一个类模板

	原子操作:
		基本认识:
			可以把原子操作理解成一种:不需要用到互斥量加锁的技术实现多线程编程方式
			原子操作:是在多线程中,不会被打断的程序执行片段,原子操作,比互斥量效率更胜一筹
			互斥量的加锁一般是针对一个代码段(多行代码),而原子操作针对的一般都是一个变量,而不是代码段
			原子操作:不可分割的操作,这种操作状态要么是完成的,要么是没完成,不可能出现半完成状态
			std::atomic 是一个类模板

		使用演示:
			std::atomic<int> mycount = 0;	// 封装了一个类型为 int 的对象,可以当作普通 int 类型来使用(但是它具备原子操作能力)
			mycount++;		// 它的操作是原子操作,不会被打断



	Windows 临界区:
		需要引入头文件:<windows.h>


	线程池:
		(1) 场景设想
			服务器程序 --> 客户端:每来一个客户端,就创建一个新线程为该客户提供服务
			a) 网络游戏,几万玩家不可能给每个玩家创建个新线程,此程序写法在这种场景下不合适
			b) 程序稳定性:在这种代码中,因为服务的需要突然偶尔来这么一下创建线程,这种不仅感觉对机器性能不友好,而且感觉非常不安全
		
		(2) 线程池概念:把一堆线程弄在一起,统一管理,线程的任务执行完了,就把线程放回线程池中,不进行回收,
			这种统一管理调度,循环利用线程的方式,就叫线程池
			
		(3) 实现原理:在程序启动时,就一次性创建好一定数量的线程

		(4) 线程创建数量:
			* 线程开的数量极限问题:2000个线程基本就是极限
*/
#include<iostream>
#include<string>
#include<thread>
#include<mutex>

using namespace std;

// 创建一个互斥量
mutex resourceMutex;

// 单例模式(懒汉式)
class MyTest
{
private:
	MyTest() {};					// 构造函数私有化————禁止外部创造对象
	static MyTest* m_instance;		// 用来存放对象指针————管理内部所创造出来的对象

public:
	static MyTest* getInstance()	// 对外提供一个创建对象的接口
	{
		if (m_instance == nullptr)	// 双重检查
		{
			lock_guard<mutex> myGuard(resourceMutex);	// 自动上锁,自动解锁
			if (m_instance == nullptr)
			{
				m_instance = new MyTest();		// 对象在堆区,需要手动释放
				static Cleaner cleaner;			// 对象在栈区,由系统回收
			}
		}
		return m_instance;
	}

	// "清理者"类(在类里面嵌套一个类)
	class Cleaner
	{
	public:
		// 当系统释放对象时,会调用析构函数,利用这个特性,“手动”释放 MyTest 对象
		~Cleaner()
		{
			if (MyTest::m_instance)
			{
				delete MyTest::m_instance;
				MyTest::m_instance = nullptr;
			}
		}
	};

	void test()
	{
		cout << "当前对象的内存地址:" << this << endl;
	}
};

void task()
{
	cout << "线程:" << this_thread::get_id() << "开始执行任务" << endl;

	auto obj = MyTest::getInstance();	// 创建一个 MyTest 对象
	obj->test();

	cout << "线程:" << this_thread::get_id() << "执行任务完毕" << endl;
}


// 类静态变量初始化
MyTest* MyTest::m_instance = nullptr;

int main()
{
	MyTest* obj;

	// 创建一个对象
	obj = MyTest::getInstance();
	obj->test();

	// 再创建一个对象
	obj = MyTest::getInstance();
	obj->test();

	// 创建两个线程
	thread t1(task);
	thread t2(task);

	t1.join();	// 让主线程等待 t1 的加入
	t2.join();	// 让主线程等待 t2 的加入

	system("pause");
	return 0;
}

快速上手

1. 基本介绍

线程

C++ 98

  • C++ 98 标准中没有提供多线程支持
  • C++ 98 中实现多线程通常要使用平台特定的 API
  • 如 Windows 的线程 API 或 POSIX 线程(pthreads)来实现多线程
  • 此外一些第三方库,如 Boost.Thread,也提供了在 C++ 98 环境中进行多线程编程的功能

C++ 11

  • C++ 11 标准中提供了<thread> 库,开始直接支持多线程编程
  • 通过引入 <thread><mutex><condition_variable> 等头文件,提供了更为方便和统一的多线程编程接口

进程

  • C++ 标准库没有提供"创建进程"的功能
  • "创建进程"通常依赖于操作系统的 API 或 其他库函数
  • 在 Unix 和 Linux 系统中,可以使用 fork() 函数来创建进程
  • 在 Windows 系统中,可以使用 CreateProcess() 函数来创建进程

2. 创建线程

#include<iostream>
#include<string>
#include<thread>


// 输出 Hello World
void printHelloWorld()
{
	std::cout << "Hello World" << std::endl;
}


// 输出特定的文本内容
void print(std::string text)
{
	std::cout << text << std::endl;
}


// 主函数
int main()
{
	// 创建一个线程 t1,让它执行 printHelloWorld 这个函数
	std::thread t1(printHelloWorld);
    

	// 等待 t1 线程完成(如果不等待,可能子线程 t1 还没完成的时候,主线程已经结束了,程序会报错)
	t1.join();


	// 创建一个线程 t2,让它执行 print 这个函数,并传入参数
	std::thread t2(print, "This is thread 2.");
    

	// 等待 t2 线程完成(如果不等待,可能子线程 t2 还没完成的时候,主线程已经结束了,程序会报错)
	t2.join();


	// 创建一个线程 t3
	std::thread t3(print, "This is thread 3.");
    

	// 分离线程(也可以使用分离线程这个技术,让主线程结束后,子线程依然可以运行)
	t3.detach();


	// 创建一个线程 t4
	std::thread t3(print, "This is thread 3.");
    

	// 严谨的项目里面,可能用到,先判断该线程是否可以被join()
	bool isJoin = t3.joinable();
	if (isJoin)
	{
		t3.join();
	}

	return 0;
}

3. 线程常见错误

  1. 传递临时变量的问题
#include<iostream>
#include<thread>


void foo(int& x)
{
	x += 1;
}

int main()
{
	int num = 1;		// 局部变量 num
	std::thread t1(foo, std::ref(num));		// std::ref() 传递引用类型
	t1.join();		// 等待t1线程结束

	std::cout << num << std::endl;

	return 0;
}
  1. 传递指针或引用指向局部变量的问题
#include<iostream>
#include<thread>

// 创建一个线程 t (全局变量)
std::thread t;
int a = 1;

void foo(int& x)
{
	std::cout << x << std::endl;		// 1
	x += 1;
	std::cout << x << std::endl;		// 2
}


void test()
{
	t = std::thread(foo, std::ref(a));
}

int main()
{
	test();

	t.join();

	return 0;
}
  1. 入口函数为类的私有成员函数
#include<iostream>
#include<thread>
#include<memory>	// 智能指针,不用的时候,会自动释放

class A
{
private:
	friend void thread_foo();
	void foo()
	{
		std::cout << "hello" << std::endl;
	}
};


void thread_foo()
{
	std::shared_ptr<A> a = std::make_shared<A>();	// 使用智能指针实例化A对象
	std::thread t(&A::foo, a);
	t.join();
}

int main()
{
	thread_foo();
}

4. 互斥量

锁的使用

#include<iostream>
#include<thread>
#include<mutex>

int a = 0;

// 创建互斥锁
std::mutex mtx;

void func()
{
	for (int i = 0; i < 10000; i++)
	{
		mtx.lock();		// 加锁
		a += 1;
		mtx.unlock();	// 解锁
	}
}

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

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

	std::cout << a << std::endl;
	return 0;
}

死锁演示

  • 图形演示

在这里插入图片描述

  • 代码演示
#include<iostream>
#include<thread>
#include<mutex>


// 创建互斥锁
std::mutex mtx1;
std::mutex mtx2;


// people1 先抢占 mtx1,再快速抢占 mtx2
void people1()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people1 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people1 上锁mtx2成功\n";
		mtx1.unlock();
		mtx2.unlock();
	}
}

// people2 先抢占 mtx2,再快速抢占 mtx1
void people2()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx2.lock();
		std::cout << "people2 上锁mtx2成功\n";
		mtx1.lock();
		std::cout << "people2 上锁mtx1成功\n";
		mtx1.unlock();
		mtx2.unlock();
	}
}


int main()
{
	std::thread t1(people1);
	std::thread t2(people2);

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

	return 0;
}

解决死锁

  • 解决办法:所有人都要严格按照顺序抢占资源,都要先抢占完A资源,才能继续抢占B资源,继续抢占C资源…
#include<iostream>
#include<thread>
#include<mutex>


// 创建互斥锁
std::mutex mtx1;
std::mutex mtx2;


// people1 要先抢占 mtx1,才能抢占 mtx2
void people1()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people1 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people1 上锁mtx2成功\n";
		mtx1.unlock();
		std::cout << "people1 解锁mtx1成功\n";
		mtx2.unlock();
		std::cout << "people1 解锁mtx2成功\n";
	}
}

// people2 要先抢占 mtx1,才能抢占 mtx2
void people2()
{
	for (int i = 0; i < 1000; i++)
	{
		mtx1.lock();
		std::cout << "people2 上锁mtx1成功\n";
		mtx2.lock();
		std::cout << "people2 上锁mtx2成功\n";
		mtx1.unlock();
		std::cout << "people2 解锁mtx1成功\n";
		mtx2.unlock();
		std::cout << "people2 解锁mtx2成功\n";
	}
}


int main()
{
	std::thread t1(people1);
	std::thread t2(people2);

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

	return 0;
}

内容推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值