内容更新
/*
基本的创建和使用线程方法:
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. 线程常见错误
- 传递临时变量的问题
#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;
}
- 传递指针或引用指向局部变量的问题
#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;
}
- 入口函数为类的私有成员函数
#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;
}