C++11 thread库
线程库的基本使用
创建线程
需要一个可调用的函数或者函数对象,作为线程入口。C++11中可以使用函数指针,函数对象或者lambda表达式实现,基本语法如下
#include <thread>
std::thread t(function_name, args...);
- function_name是线程入口点的函数或可调用对象
- args...是传递给函数的参数
创建后可以使用.join()函数(阻塞式的)保证线程完成后才退出进程。 - .detach()分离线程
std::thread thread2(printString, "HELLO C++");
//分离线程,独立于主线程运行,但是由于创建在main中,主线程结束后被销毁了
thread2.detach();
- .joinable()返回一个bool值,判断线程是否可以调用join或者detach
std::thread thread1(printHelloWord);
//判断线程是否可以调用join或者detach
bool isJoin = thread1.joinable();
if(isJoin)
//主程序等待此线程执行完毕
thread1.join();
线程函数中未定义错误
1.传递临时变量的问题
- std::ret()传递引用类型
#include <bits/stdc++.h>
#include <thread>
using namespace std;
void foo(int& x) {
x += 1;
return;
}
signed main() {
int a = 1;
//这样会报错
//thread t1(foo, a);
//使用std::ref()
thread t2(foo, std::ref(a));
t.join();
cout << a << endl;
return 0;
}
2.传递指针或引用指向局部变量的问题
错误示例:
#include <bits/stdc++.h>
#include <thread>
using namespace std;
thread t;
void foo(int& x) {
x += 1;
return;
}
void test() {
int a = 1;
//这里使用线程的有参构造
t = thread(foo, std::ref(a));
return;
//在这里return后局部变量a已经被释放掉了
}
signed main() {
test();
//此时线程t取不到a了
t.join();
return 0;
}
更正:
//将a改为全局变量
int a = 1;
void test() {
//这里使用线程的有参构造
t = thread(foo, std::ref(a));
return;
}
3.传递指针或指向已释放的内存的问题
错误示例:
#include <bits/stdc++.h>
#include <thread>
using namespace std;
void foo(int *x) {
cout << *x << endl;
return;
}
signed main() {
int* ptr = new int(1);
thread t(foo, ptr);
//与之前一样,一个是操作系统释放的,一个是你释放的,此时读不到了
delete(ptr);
ptr = NULL;
t.join();
return 0;
}
改为:
t.join();
delete(ptr);
ptr = NULL;
return 0;
4.类成员函数作为入口函数,类对象被提前释放
创建线程之后对象被立刻销毁,这回导致线程在执行时无法访问obj对象,可能会导致程序奔溃或产生未定义的行为
使用智能指针,可以在变量不被需要的时候自动释放。
#include <bits/stdc++.h>
#include <thread>
#include <memory>
using namespace std;
class A {
public:
void foo(){
cout << "HELLO" << endl;
}
};
signed main() {
//智能指针,指向class A对象a的智能指针,可以在变量不被需要时被自动释放掉
shared_ptr<A> a = make_shared<A>();
thread t(&A::foo, a);
t.join();
return 0;
}
线程创建部分
&A::foo
这部分表示要在线程中执行的函数。A
是类的名称,而foo
是类A
中的一个成员函数。这里使用了成员函数指针&A::foo
,表明线程将执行a
对象的foo
成员函数。a
是一个对象,这个对象是类A的一个实例。线程将在这个对象上调用成员函数foo
。
综合起来,这行代码的作用是创建一个线程对象 thread
,该线程将在后台执行对象 a
的 A
类中的foo
成员函数
智能指针
std::shared_ptr
这部分定义了一个共享指针,用于管理指向类型A
的对象的所有权。共享指针是智能指针的一种,它可以在多个共享指针之间共享同一个对象,并且能够自动管理对象的生命周期。ptr
这是指向类型A
的共享指针的名称。你可以通过这个指针来访问和管理 A 类的对象。std::make_shared()
这是使用std::make_shared
函数来创建类型A
的对象,并返回一个指向该对象的共享指针。std::make_shared
是一个安全的方式来分配动态内存,并返回一个指向该内存的共享指针。
5.入口函数为类的私有成员函数
#include <bits/stdc++.h>
#include <thread>
#include <memory>
using namespace std;
//要让外部的函数可以调用到类的私有变量或私有成员函数需使用友元
class A {
private:
friend void thread_foo();
void foo(){
cout << "HELLO" << endl;
return;
}
};
void thread_foo() {
shared_ptr<A> a = make_shared<A>();
thread t(&A::foo, a);
t.join();
return;
}
signed main() {
thread_foo();
system("pause");
return 0;
}
互斥量解决多线程数据共享问题
数据共享问题
在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问一个变量,并且其中至少有一个线程对此变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃,产生未定义结果,或者得到错误结果。
比如两个线程同时取出某个变量对其进行加操作,可能会导致此变量的值只增加了一。
需要当一个线程访问一个变量的时候此变量不可被其他线程访问。
互斥锁
mutex
std::mutex mtx;
mtx.lock();
a += 1;
//解锁
mtx.unlock();
#include <bits/stdc++.h>
#include <thread>
//互斥锁
#include <mutex>
using namespace std;
int a = 0;
//互斥锁
std::mutex mtx;
void func() {
for (int i = 0; i < 10000; ++i) {
//对a进行加锁操作
mtx.lock();
a += 1;
//解锁
mtx.unlock();
}
}
signed main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
return 0;
}
多线程运行结果每一次都与单线程一样的就被称为线程安全
互斥量死锁
场景
假设两个线程T1与T2,它们需要对两个互斥量mtx1与mtx2进行访问,而且需要按以下顺序获取互斥量的所有权:
- T1先获取mtx1的所有权,再获取mtx2的所有权
- T2先获取mtx2的所有权,再获取mtx1的所有权
如果两个线程同时执行会出现死锁问题。因为T1获取了mtx1的所有权,但是无法获取mtx2的所有权,而T2获取了mtx2的所有权,但是无法获取mtx1的所有权,两个线程互相等待对方释放互斥量,导致死锁。
例子:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
#include <windows.h>
using namespace std;
//互斥锁
std::mutex m1, m2;
void func1() {
m1.lock();
Sleep(10);
m2.lock();
m1.unlock();
m2.unlock();
}
void func2() {
m2.lock();
Sleep(10);
m1.lock();
m2.unlock();
m1.unlock();
}
signed main() {
thread t1(func1);
thread t2(func2);
t1.join();
t2.join();
cout << "over" << endl;
return 0;
}
解决方法:
void func1() {
m1.lock();
Sleep(10);
m2.lock();
m1.unlock();
m2.unlock();
}
void func2() {
m1.lock();
Sleep(10);
m2.lock();
m1.unlock();
m2.unlock();
}
lock_guard 与 C++11 unique_lock
lock_guard
std::lock_guard
为C++标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题
特性
- 构造函数被调用时,该互斥量会被自动锁定
- 当析构函数被调用时,该互斥量会被自动解锁
std::lock_guard
对象不能复制或移动,因此它只能在局部作用域中使用,它禁用了等于号与拷贝构造
申明时会自动加锁,在lock_guard
作用域结束后会自动解锁
例子:
#include <bits/stdc++.h>
#include <thread>
//互斥锁
#include <mutex>
using namespace std;
int share_data = 0;
std::mutex mtx;
void func1() {
for (int i = 0; i < 5000; i++) {
//如果阻塞在这里一段时间,会自动结束调用析构函数
lock_guard<mutex> lg(mtx);
share_data += 1;
}
}
signed main() {
thread t1(func1);
thread t2(func1);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
lock_guard
的构造与析构函数:
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
构造函数的重载函数
std::lock_guard<std::mutex> lg(mtx, std::adopt_lock);
//这里表示此互斥量已经加过锁了,无需经由构造函数加锁
unique_lock
std::unique_lock
为C++标准库中的一种互斥量封装类,用于对互斥量进行加锁与解锁操作,可更加灵活的管理,包括延迟加锁,条件变量,超时等
特性
unique_lock
更加灵活,一般使用unique_lock
,其在构造的时候也会自动对互斥量进行加锁,析构时自动解锁,不想自动加锁解锁时在传入变量时要多加一个变量std::defer_lock()
lock()
:
尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。try_lock()
:
尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true。try_lock_for(const std::chrono::duration<Rep, Period>& rel_time)
:
尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time)
:
尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。unlock()
:
对互斥量进行解锁操作。swap()
:
支持swap()
变量交换。
自动加锁:
std::mutex mtx;
void func1() {
for (int i = 0; i < 10000; i++) {
//自动加锁
unique_lock<mutex, defer_lock> lg(mtx);
share_data += 1;
}
}
手动加锁:
std::mutex mtx;
void func1() {
for (int i = 0; i < 10000; i++) {
//表示构造但是不加锁,但可以自动解锁
unique_lock<mutex, defer_lock> lg(mtx, defer_lock);
//手动加锁
lg.lock();
share_data += 1;
}
}
try_lock_for
:
有时间操作时mutex
不支持,需换成支持时间操作的timed_mutex
如果没有获取加锁所有权,等待指定时间后直接不等了,直接返回
std::timed_mutex mtx;
unique_lock<timed_mutex> lg(mtx, defer_lock);
lg.try_lock_for(std::chrono::seconds(2));
call_once与其使用场景
单例模式是一种常见设计模式,用于确保某个类只能创建一个实例(全局只有一个对象的类),由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时需考虑线程安全问题。
懒汉模式会出现多个线程同时创建单例对象。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
once_flag g_flag;
class Base {
public:
Base(const Base& obj) = delete;
Base& operator = (const Base& obj) = delete;
static Base* getInstance() {
//保证实例对象只有一个
//传入匿名函数
call_once(g_flag, [&]() {
obj = new Base;
cout << "实例对象被创建" << endl;
});
return obj;
}
void set(string name) {
this->name = name;
}
string getName() {
return this->name;
}
private:
Base() {};
static Base* obj;
string name;
};
//类外初始化静态变量
Base* Base::obj = nullptr;
void myFunc(string name){
Base::getInstance()->setName(name);
cout << "My name is: " << Base::getInstance()->getName() << endl;
}
int main() {
thread t1(myFunc, "john");
thread t2(myFunc, "lily");
t1.join();
t2.join();
return 0;
}
condition_variable与其使用场景
生产者与消费者模型
生产者向任务队列加任务,消费者不断向任务队列取任务去完成
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
#include <string>
#include <condition_variable>
#include <queue>
std::mutex mtx;
//任务队列
std::queue<int> g_queue;
std::condition_variable g_cv;
void Producer() {
for (int i = 0; i < 10; ++i) {
{
std::unique_lock<std::mutex> lock(mtx);
g_queue.push(i);
std::cout << "task : " << i << std::endl;
}
}
}
void Consumer() {
while (1) {
std::unique_lock<std::mutex> lock(mtx);
bool isempty = g_queue.empty();
//如果队列为空,等待
g_cv.wait(lock, [] () {
return !g_queue.empty();
});
int value = g_queue.front();
g_queue.pop();
}
}
int main() {
return 0;
}
本文作者:Breadcheese
本文链接:https://www.cnblogs.com/breadcheese/p/18381379
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步