本文主要是介绍C++11 Thread线程池、死锁、并发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、线程与进程
进程:运行中的程序
线程:进程中的小进程
二、线程库的使用
包含头文件#include<thread>
2.1 thread函数
具体代码:
void show(string str) {cout << "This is my word : " << str << endl;
}int main() {thread thread1(show, str);return 0;
}
函数声明:std::thread thread(Function *funp, 函数所需参数)
解释:1.函数参数为一个函数名称,进程运行后会创建一个线程并执行函数内容
2. 返回值类型是一个thread类型的对象
注意:单使用thread程序会报错,应该配合join函数使用,请看下文
2.2 join函数
具体代码:
void show() {cout << "This is my code." << endl;
}int main() {thread thread1(show);thread1.join();return 0;
}
解释:如果不使用join,则show函数在打印完成之前,可能主函数main就已经return了。
当一个子线程调用join函数时,会阻塞主线程,直到当前子线程执行结束后,才
继续执行主线程。
注意:join只会保证主线程阻塞,等待自己执行完。但两个子线程并不会因为彼此的join而互相阻塞,而是在同时进行。
2.3 分离线程detach函数
分离函数是指,主函数执行完毕后,需要保持子线程依旧在执行,并且程序不报错。
具体代码:
void show(string str) {cout << "This is my code:" << str << endl;
}int main() {thread thread1(show, "Jacker");thread1.detach();return 0;
}
解释:一旦线程被分离出去,它就不再受原线程的控制和影响。因此,无法通过原线程
来获取该线程的执行结果或等待其结束。
2.4 joinable函数
具体代码:
void show(string str) {cout << "This is my code:" << str << endl;
}int main() {thread thread1(show, "Jacker");if(thread1.joinable()) {thread1.detach();}return 0;
}
解释:判断当前线程是否可以调用join()或detach()
如果未判断,系统可能报错:SYSTEM_ERROR
2.5 ref函数
ref函数可以将对象的引用传递给线程。
首先我们先来看以下代码,编译有错误:
void add(int& num) {for (int i = 0; i < 10000; i++) {num += 1;}
}int main() {int num = 0;thread t1(add, num);if (t1.joinable()) {t1.detach();}std::cout << "num : " << num << std::endl;return 0;
}
错误原因:以下是thread构造函数的声明
_NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
可以看到,当我们传入num实参时,会被转换为右值引用_Args&&类型,但实际add函数中的参数为左值引用类型,二者不匹配,因此发生了错误。
此时,只要将thread的第二个参数,从num改成std::ref(num),即可解决问题。具体如下:
thread t1(add, std::ref(num));
三、互斥量
什么是线程安全?
答:当单线程执行的结果和多线程执行的结果相同时,线程安全。
3.1 共享竞争问题
共享竞争问题指:当多个进程共享一个资源,而其中的一个进程对资源进行了写操作。此时其他进程访问到的可能是写之前的数据,导致了共享数据不同步的问题。
具体问题代码:
void add(int& num) {for (int i = 0; i < 10000; i++) {num += 1;}
}int main() {int num = 0;thread t1(add, ref(num));thread t2(add, ref(num));if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}std::cout << "num : " << num << std::endl;return 0;
}
如果程序按照逻辑,执行结束后num应该是20000,但多次执行,会发现结果每次都不同,而且均在10000-20000之间。
解释:这就是竞争产生的问题,因为num为t1和t2的共享资源。
解决方法:采用互斥锁。需将共享资源num设置为:如果有线程访问,则其他线程不允许访问的状态。在共享时,进行加锁。在共享结束后,在进行解锁。
头文件 | #include<mutex> |
初始化一个锁 | std::mutex mtx; |
加锁 | mtx.lock() |
解锁 | mtx.unlock() |
结合刚才的错误代码,修正后如下所示:
std::mutex mtx;void add(int& num) {for (int i = 0; i < 10000; i++) {mtx.lock();num += 1;mtx.unlock();}
}int main() {int num = 0;thread t1(add, ref(num));thread t2(add, ref(num));if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}std::cout << "num : " << num << std::endl;return 0;
}
3.2 互斥量死锁
现有2个线程t1与t2,还有2个互斥量m1和m2,t1要先访问m1再访问m2,t2要先访问m2再访问m1。(一个互斥量在同一时间,只能被一个占用)
(占用:对m加了锁,但还没解锁,此时其他线程不可以再对m进行加锁)
当他们同时访问时,t1先占用了m1的所有权,同时t2占用了m2的所有权,此时t1和t2都进行不了第二步,互相卡死,这种情况称为互斥量死锁。
具备互斥量死锁隐患的具体代码如下所示:(线程不安全)
//注意:以下代码只是具备隐患,执行可能成功,也可以卡死mutex m1;
mutex m2;//线程1所执行的函数
void func1() {m1.lock();m2.lock();cout << "Do func1" << endl;m1.unlock();m2.unlock();
}//线程2所执行的函数
void func2() {m2.lock();m1.lock();cout << "Do func2" << endl;m2.unlock();m1.unlock();
}int main() {int num = 0;thread t1(func1);thread t2(func2);if (t1.joinable()) {t1.join();}if (t2.joinable()) {t2.join();}cout << "Over" << endl;return 0;
}
解决方法:调整加锁和解锁的顺序(要视具体情况而定)
四、lock_guard和unique_lock
4.1 lock_guard
lock_guard是一个互斥量的模板类,具有以下特征:
std::mutex mtx;//互斥量
std::lock_guard<std::mutex> lg(mtx);//自动加锁
//作用域结束后,执行析构函数,自动解锁
1. 构造函数传入一个互斥量,会对其进行自动加锁
2. 当析构函数被调用时,该互斥量会自动解锁
3. lock_guard对象不能复制或移动,只能在局部作用域中使用
针对3.1中的例子,修改后使用lock_guard为:
void add(int &num) {for(int i = 0; i < 10000; i++) {std::lock_guard<std::mutex> lg(mtx);//自动mtx加锁num++;//此处lg作用域结束,mtx自动解锁}
}
以下是lock_guard的原码:
_EXPORT_STD template <class _Mutex>
class _NODISCARD_LOCK lock_guard { // class with destructor that unlocks a mutex
public:using mutex_type = _Mutex;explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock_MyMutex.lock();}lock_guard(_Mutex& _Mtx, adopt_lock_t) noexcept // strengthened: _MyMutex(_Mtx) {} // construct but don't lock~lock_guard() noexcept {_MyMutex.unlock();}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;private:_Mutex& _MyMutex;
};
4.2 unique_lock
unique_lock是一个封装了互斥量的类模板,不可以复制。它可以对互斥量进行更多的管理:延迟加锁等。此时就需要在构造函数不能自动加锁,应该使用二参构造函数,之后按需手动加锁:
std::unique_lock<std::mutex> lg(mtx, std::defer_lock);//此处没有自动加锁
lg.lock();//手动加锁
unique_lock常用的函数成员如下所示:
成员函数名称 | 解释 |
lock() | 对互斥量进行加锁。 如果互斥量已经被其他线程所占有,则阻塞本线程,直到加锁成功 |
try_lock() | 尝试对互斥量进行加锁。 如果互斥量已经被其他线程所占有,则返回 false,成功加锁返回 true |
try_lock_for( const std::chrono:: duration<Rep, Period>& interval) | 对互斥量进行加锁。 如果互斥量已经被其他线程所占有,则阻塞本线程, 直到加锁成功或 超过指定的时间间隔 (超过了1分钟30秒) |
try_lock_until( const std::chrono:: time_point <Clock, Duration>& time) | 对互斥量进行加锁。 如果互斥量已经被其他线程所占有,则阻塞本线程, 直到加锁成功或 超过指定的时间 (超过了12:33:54) |
unlock() | 对互斥量进行解锁。 |
举例说明,假设需要在5秒内加锁,如果超过5秒还没加锁,则直接返回:
timed_mutex m1;void add(int& num) {for (int i = 0; i < 10000; i++) {unique_lock<timed_mutex> lg(m1, std::defer_lock);lg.try_lock_for(std::chrono::seconds(5));num++;}
}
五、单例设计模式
单例设计模式是确保某个类只能创建一个实例。(比如日志类:Log)
由于单例模式是全局唯一的,因此在多线程环境下需考虑线程安全的问题。
一个单例设计模式的类具有以下特征:
1. 禁用拷贝构造函数(设置为private权限)
2. 禁用 = 运算符重载
3.写一个静态方法获取静态对象
4. 以此对象调用静态成员函数
5.1 饿汉模式
饿汉模式指直接在类的内部实例化一个对象,通过静态方法返回这个对象的引用。
(饿汉急不可耐,类加载后就马上创建了对象)
具体代码:
class Log {
public :static Log& getInstance() {static Log log;//饿汉模式return log;}static void printMsg(string message) {cout << message << endl;}private:Log() {}Log& operator=(const Log& log) {}
};
5.2 懒汉模式
懒汉模式是指在类的内部先声明一个指针,在调用静态方法时再动态申请new出具体的空间,最后返回这个指针指向的空间,也就是对象的引用。
(懒汉只有需要时候才做事情,只有调用静态函数时才申请空间)
(懒汉模式由于需要动态申请,所以线程不安全)
具体代码:
class Log {
public :static Log& getInstance() {static Log *log = nullptr;//懒汉模式if(!log) {log = new Log;}return *log;}static void printMsg(string message) {cout << message << endl;}private:Log() {}Log& operator=(const Log& log) {}
};
5.3 call_once
call_once函数的声明如下:
函数声明 | void call_once(flag, func, Args); |
解释 | call_once()保证在多个线程调用一个函数时,同时只能有一个调用成功,其他需要等待 flag是once_flag的一个对象,表示标记函数是否已经被调用过 func是需要被调用的函数 Args为需要被调用的函数参数 |
(这里建议直接对线程不安全的代码加锁,以保证线程安全)
六、条件变量
(需要包含头文件#include<condition_variable>)
C++的条件变量condition_variable的使用步骤如下:
1. 创建一个std::condition_variable对象
2. 创建一个互斥锁mutex对象,用于保护共享资源
3. 在需要等待条件变量的地方,使用unique_lock对象锁定互斥锁
并调用std::condition_variable::wait()、std::condition_variable::wait_for()
或 std::condition_variable::wait_until()等待条件变量 (阻塞)
4. 在其他线程中需要唤醒 “等待线程” 时,调用std::condition_variable::notify_one()
或 std::condition_variable::notify_all()通知等待的线程 (取消阻塞)
(注意:wait函数在阻塞时,会自动解锁。)
结合一个具体的生产者-消费者模型更好理解:(请注意看注释理解)
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>std::condition_variable g_cv;//条件变量
std::queue<int> q;//任务队列
std::mutex mtx;//互斥锁//生产者
void producer() {for (int i = 0; i < 10; i++) {//总共生产10个任务/* 这里解释一下为什么要增加大括号?*//* unique_lock是作用域结束后自动放开锁的 *//* 如果不加大括号,那个暂停的 1ms 期间也没有释放锁,此时消费者无法获取任务,*//*这样做程序,影响并发性,就是效率低 */{ std::unique_lock<std::mutex> lock(mtx);//为 放任务 加锁/* 在生产者需要唤醒“等待线程”(判断队列是否满)的地方,使用notify_one() */g_cv.notify_one();q.push(i);//放任务std::cout << "生产者放入了一个" << i << std::endl;}//暂停1毫秒,避免生产的太快,消费者还没来得及拿第一个,生产者就已经放完10个任务了std::this_thread::sleep_for(std::chrono::microseconds(1));}
}//消费者
void consumer() {int count = 0;//用于计算完成任务的总数,为10时结束程序while (true) {std::unique_lock<std::mutex> lock(mtx);//为 取任务 加锁/* 在消费者需要等待(判断队列是否为空)的地方,使用wait() */g_cv.wait(lock, []() { //第二个参数是lamba表达式,一元谓词(返回值类型为bool)return !q.empty();});int task = q.front();//取任务q.pop();std::cout << "消费者拿走并完成了一个" << task << std::endl;//当完成任务的总数等于10,则跳出循环if (++count == 10) {break;}}
}int main() {std::thread t_pro(producer);//生产者线程std::thread t_con(consumer);//消费者线程if (t_pro.joinable()) {t_pro.join();}if (t_con.joinable()) {t_con.join();}return 0;
}
七、跨平台线程池
这篇关于C++11 Thread线程池、死锁、并发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!