C++11的新特性—线程库,原子操作,条件变量
(2025-10-28 11:49:48)
标签:
it |
分类: 技术 |
https://cloud.tencent.com/developer/article/2443934
【Linux】从零开始认识多线程 — 线程概念与底层实现
【Linux】从零开始认识多线程 — 线程控制
【Linux】从零开始认识多线程 — 线程ID
【Linux】从零开始认识多线程 — 线程互斥
默认构造是创建一个无参的空线程。一般创建时要传入需要执行的函数方法,和一个参数包!在linux下,如果我们想要传入多个参数,就要想办法将这些参数进行一个整合,即在堆上开辟一个结构体来让线程获取。而在C++11中,不需要进行结构体的传递,通过可变参数包的方法就可以满足!
获取线程id接口 get_id:
并行执行会出现一些换行的问题!
当对同一个全局变量进行操作时,如果操作不是原子的,就很有可能导致一些错误,这些错误是偶发性的,不容易复刻。我们来通过一个大数据情况下的运行看看:
为了避免这样运行的错误,我们可以进行加锁:
因为这里 x ,
tex不是直接传入到Print函数中的,而是进行thread构造,里面会有这些参数,因为是一个左值引用也就是进行一个深拷贝。thread的底层是pthread_create,需要特定类型的函数指针和参数:
所以为了可以保证一直是引用,可以使用ref()保证传值拷贝的时候是引用
我们使用lambda表达式进行所有变量的引用捕捉,就能获取到x tex
其中mutex中的接口有:
其底层是cas先比较再设置,保证操作的原子性!
我们来试着使用一下:
获取其中的数据可以使用load接口,修改数据可以使用exchange接口…
创建10个线程,都有对应1 - 10
的ID号,每次只能打印一个线程的id,如果ready为真就wait
,为假才继续进行。进行打印之前将ready设置为真,打印结束设置为假!
这里我们加入一个计时的接口this_thread::sleep_for(std::chrono::seconds(
))可以进行等待!
很顺利!这时两个线程的情况,如果有多个进程,可以通过宏定义一些数字,每个线程任务对应一个数字。变量满足时才进行执行任务!这样就会让不符合条件的变量阻塞在条件变量或者阻塞在获取锁中!通过这样的调控,可以满足多线程情况下的并发需求!
1 线程
1.1 线程概念
在Linux中我们了解了什么是线程:
线程:在进程内部运行,是CPU调度的基本单位,共享一个地址空间。Linux下线程本质是一种轻量化进程,可以在一个进程中并发运行不同的任务。同时Linux为了避免出现同时访问的问题,保证线程互斥,可以加入互斥锁!
在语言层,每个语言都封装了线程库,内部封装了底层的系统调用,让上层更加方便的使用。
1.2 C++中的线程
c++中线程被设计成了一个类来方便我们的使用:
我们可以快捷通过创建一个对象来快速创建线程,也可以调用对象的join接口来进行等待!
我们来看构造函数:
来看一个例子:
void Print(int n)
{
for (int i = 0; i < n;
i++)
{
cout << i <<
endl;
}
cout <<
endl;
}
int main()
{
thread t(Print ,
100);
t.join();
return 0;
}
我们构造的时候加入print和100,就可以单独设置一个线程来运行Print函数。
主线程中获取新线程的id直接进行调用即可
Print接口中获取自己的id就比较复杂,因为Print函数中并没有线程对象?那怎么办可以通过this_thread类(全局的一些数据)。可以通过this_thread的get_id来获取主线程id,来侧面验证
1.3 线程并行
再来运行两个线程我们来看看:
mutex类是对底层的锁的系统调用进行的封装
通过lock和unlock可以快捷的进行上锁解锁!
int x = 0;
//全局锁
mutex tex;
void Print(int n)
{
//这个for循环不是临界区,处在线程独立的栈中
for (int i = 0; i < n;
i++)
{
tex.lock();
//对全局数据的操作是临界的
x++;
tex.unlock();
}
}
int main()
{
thread t1(Print,
10000);
thread t2(Print,
20000);
t1.join();
t2.join();
cout << x <<
endl;
return 0;
}
在进入临界区之前加锁,保证非原子的操作中不会受到其他线程的打扰!同样的也会发生频繁的上下文切换,导致运行效率变低!
如果不使用全局变量呢?我们可以通过参数传递过去:
void Print(int n , int& rx , mutex& rtex)
{
for (int i = 0; i < n;
i++)
{
tex.lock();
x++;
tex.unlock();
}
}
int main()
{
mutex tex;
int x = 0;
//这样无法直接传入过去
thread t1(Print, 10000 , x
, tex);
thread t2(Print, 20000 , x
, tex);
t1.join();
t2.join();
cout << x <<
endl;
return 0;
}
当我们向这样运行的时候,就会发现,报错了!
thread t1(Print, 10000 ,
ref(x) , ref(tex));
thread t2(Print, 20000 ,
ref(x) , ref(tex));
这样就可以正常运行了!这里略显麻烦,为了适配底层的系统调用(C语言版本)需要付出一些代价!
我们再来看一个混合使用:lambda表达式,十分的优雅!
int x = 0;
mutex tex;
//捕捉所有的
thread
t1([&]()
{
for (int i = 0; i <
10000; i++)
{
tex.lock();
x++;
tex.unlock();
}
});
thread
t2([&]()
{
for (int i = 0; i <
20000; i++)
{
tex.lock();
x++;
tex.unlock();
}
});
t1.join();
t2.join();
优雅!
再来看:如果我们想要创建很多线程,但是先不使用。等待后续才进行使用。我们可以使用vector容器来进行储存线程,需要时就进行遍历来获取空的线程对象,对空的线程对象进行移动赋值!
int main()
{
vector vthd;
int n = 0;
cin >>
n;
//进行初始化
vthd.resize(n);
int x = 0;
mutex tex;
auto func = [&](int
n)
{
for (int i = 0; i < n;
i++)
{
tex.lock();
x++;
tex.unlock();
}
};
for (auto& thd :
vthd)
{
//移动赋值
thd = thread(func,
10000);
}
for (auto& thd :
vthd)
{
thd.join();
}
cout << x <<
endl;
return 0;
}
优雅!
1.4 锁
C++11中提供了很多种锁:
lock:上锁 — 阻塞的,没锁可以使用就进行阻塞
unlock:解锁
try_lock:上锁 — 非阻塞的 ,没有锁可用就返回false
其中timed_mutex,在mutex的基础上加入了时间限定函数:
try_lock_for :可以设置上锁的时间
try_lock_until : 上锁到对应时间点
其中recursive_mutex递归锁,可以在递归函数中进行使用,防止死锁的问题!
实际用法到具体使用时在细细研究就好!
我们再来看一个比较巧妙的方法:
class LockGuard
{
public:
LockGuard(mutex&
mtx):
_mtx(mtx)
{
_mtx.lock();
}
~LockGuard()
{
_mtx.unlock();
}
private:
mutex&
_mtx;
};
通过这个类,我们可以在临界区前创建一个锁守卫,生命周期结束就会自动解锁!为了不会锁住非临界区的数据,可以使用{
}划定局部域!库中提供了模版锁守卫lock_guard,可以方便使用!
2 原子操作
我们需要进行一些非原子操作的时候,比如++,或者修改一个全局的flag。使用锁操作有些大炮打蚊子的感觉,这时可以使用原子操作来进行!
atomic x = 0;
auto func = [&](int n)
{
for (int i = 0; i < n;
i++)
{
x++;
}
};
这样就了可以保证操作的原子性了,比加锁简单多了!
3 条件变量
条件变量经常使用在多线程环境下,它允许线程在某些条件不满足时挂起(等待),直到另一个线程更新了共享数据并通知条件变量,使得等待的线程可以继续执行。
条件变量主要提供以下接口:
wait():阻塞当前线程,直到条件变量被唤醒,通常在互斥锁锁定的情况下调用,进入wait之前会进行一个解锁!
wait_for():阻塞当前线程,直到条件变量被唤醒或给定的时间超时。
wait_until():阻塞当前线程,直到条件变量被唤醒或到达某个特定的时间点。
notify_one:通知一个线程开始工作,如果等待的超过1个,就会进行随机选择!
notify_all:唤醒所有线程
我们来看一个例子:
我们来实现:两个线程交替打印奇偶数,我们来通过这个了解条件变量:
#include
// std::cout
#include
// std::thread
#include
// std::mutex,
std::unique_lock
#include // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock
lck(mtx);
while (!ready)
cv.wait(lck);
// ...
std::cout << "thread
" << id << '\n';
}
void go() {
std::unique_lock
lck(mtx);
ready = true;
cv.notify_all();
}
int main()
{
//创建10个线程
std::thread
threads[10];
// spawn 10
threads:
for (int i = 0; i < 10;
++i)
threads[i] =
std::thread(print_id, i);
std::cout << "10
threads ready to race...\n";
//休眠保证所有线程进入wait
this_thread::sleep_for(std::chrono::seconds(
1 ))
go();
// go!
for (auto& th :
threads) th.join();
return 0;
}
这样进行的打印就是随机的了,因为调用的顺序不确定!
我们需要创建两个线程来分别执行打印奇数和打印偶数
一定要保证一个线程打印了一个 ,另一个线程才能打印一个!此时就是一个类似进程间通信的场景,为 false 打印偶数 , 为
true 打印奇数!每次打印都进行调整状态,帮助按照顺序进行打印!
条件变量的作用是在变量不符合条件时进行阻塞,等待变量才进行!
int main()
{
mutex mtx;
condition_variable
c;
int n = 100;
//为 false 打印偶数
//为 true 打印奇数
bool flag =
false;
thread
t1([&]()
{
int i = 0;
while (i <
n)
{
unique_lock
lock(mtx);
//只要flag ==
false就进行阻塞
//防止连续打印
while (flag)
c.wait(lock);
cout << i <<
endl;
flag = true;
i += 2;
c.notify_one();
}
});
thread
t2([&]()
{
int i = 1;
while (i <
n)
{
unique_lock
lock(mtx);
//只要flag ==
true就进行阻塞
while (!flag)
c.wait(lock);
cout << i <<
endl;
flag = false;
i += 2;
c.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
来看效果:

加载中…