文章目录
- 线程库(thread)
- 线程安全
- 锁
- 实现两个线程交替打印1-100
线程库(thread)
在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。
- 常见的接口
成员函数 | 功能 |
---|---|
join | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
get_id | 获取线程id |
detach | 将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待 |
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
注意:get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类。
thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
- 使用
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t1;//thread提供了无参的构造函数
t1 = thread(func, 10);//thread提供了移动赋值函数
thread t3 = thread(func, 10);//移动构造函数
t1.join();
t3.join();
return 0;
}
thread的带参的构造函数的定义如下:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
fn
:可调用对象,比如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。args...
:调用可调用对象fn时所需要的若干参数。
调用带参的构造函数创建线程对象:
void func(int n)
{
for (int i = 0; i <= n; i++)
{
cout << i << endl;
}
}
int main()
{
thread t2(func, 10);
t2.join();
return 0;
}
利用Lambda表达式创建n个线程对象,每个线程跑m次,同时打印出线程的id,调用thread的成员函数get_id
可以获取线程的id,但该方法必须通过线程对象来调用get_id
函数,如果要在线程对象关联的线程函数中获取线程id,也就是没有对象的情况下,可以调用this_thread
命名空间下的get_id
函数:
int main()
{
int n,m;
cin >> n>>m;
vector<thread> v;
v.resize(n);
for (auto& t : v)//拷贝构造不被允许,&
{
t = thread([m] {
for (size_t i = 0; i < m; i++)
{
cout << this_thread::get_id() << ":" <<i<< endl;
}
});
}
for (auto& t : v)
{
t.join();
}
return 0;
}
线程安全
两个线程对同一个变量进行加加
static int val = 0;
void fun1(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
void fun2(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
int main()
{
thread t1(fun1, 100000);
thread t2(fun2, 200000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
结果本来是应该加到300000,现在却是没有,这是因为++的操作不是原子性的。
为了解决这个问题:我们可以选择进行加锁
!
- 加锁
#include <mutex>
static int val = 0;
mutex mtx;
void fun1(int n)
{
mtx.lock();
for (int i = 0; i < n; i++)
{
val++;
}
mtx.unlock();
}
void fun2(int n)
{
mtx.lock();
for (int i = 0; i < n; i++)
{
val++;
}
mtx.unlock();
}
int main()
{
thread t1(fun1, 1000000);
thread t2(fun2, 2000000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
注意加锁的位置:加锁与解锁的位置可以放在for循环内也可以放循环外,那到底选择哪个位置比较好:加锁与解锁也是有消耗的,所以加锁和解锁放在for循环外边比较好
当然两个线程也是可以调用同一个函数的,这是因为每个线程都会有独立的栈结构来保存私有数据,数据不会互相干扰
- 原子性操作
#include <atmoic>
atomic<int> aval = 0;
void func1(int n)
{
for (size_t i = 0; i < n; i++)
{
++aval;
}
}
int main()
{
int m = 1000000;
thread t1(func1, 2 * m);
thread t2(func1, m);
t1.join();
t2.join();
cout << aval << endl;
return 0;
}
这里有可能val++会被放弃,以此来保证线程安全而在实际中要尽量避免使用全局变量。
int main()
{
int m = 1000000;
atomic<int> aval = 0;
auto func = [&aval](int n){
for (int i = 0; i < n; i++)
{
++aval;
}
};
thread t1(func, m * 2);
thread t2(func, m);
t1.join();
t2.join();
cout << aval << endl;
return 0;
}
- CAS操作
原子操作是CAS提供的,有相关的接口,CAS全称compare and swap是一种原子操作,多线程非阻塞地对共享资源进行修改,但是同一时刻只有一个线程可以修改,基本原理是:先比较内存中某个值是否等于预期值,如果相等,则将新值写入内存,并返回成功;否则什么也不做,并返回失败。
锁
lock与try_lock的区别
lock的加锁过程:如果没有锁就申请锁,如果其他线程持有锁就会阻塞等待,直到其他线程unlock。
try_lock就可以不让线程阻塞,如果申请不了就可以去干其他的事情。成功返回true,失败返回false。
recursive_mutex
递归互斥锁:如果在递归函数中我们想要正常用lock加锁,很可能能会导致死锁。因为上锁后递归到下一层,锁并没有被解开,相当于自己上了锁以后又申请锁。使用recursive_mutex就可以避免这种情况。递归到下一层后遇到加锁,就先判断线程的id值,如果一样就不用加锁,直接走接下来的流程。
lock_guard RAII锁:
RAII:RAII是一种C++编程中的技术,用于管理资源的生命周期,RAII在构造函数中获取资源,并在构造函数中释放资源,以此确保使用资源的对象总是处于有效状态的,这种方式减少内存泄漏的风险。
加锁后会抛出异常,那么就可能会导致锁没有被释放。为了避免这种情况,我们可以把锁封装一下,在析构函数中就可以加上解锁,这样出了作用域就可以自动销毁。具体实现:mutex的封装
当然C++线程库中也给我们提供了这样一把锁lock_guard:
int main()
{
int val = 0;
mutex mtx;
auto func = [&](int n) {
lock_guard<mutex> lock(mtx);
for (int i = 0; i < n; i++)
{
val++;
}
};
thread t1(func, 100000);
thread t2(func, 200000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
unique_lock主动解锁
与lock_guard的区别就是lock_guard只能实现RAII,lock_guard 在构造时就自动获取锁,在析构时自动释放锁,更适合对简单场景进行上锁和解锁操作;而 unique_lock 则允许在任何时候手动控制锁的加锁和解锁
实现两个线程交替打印1-100
尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。
if判断实现交替写法:
int main()
{
int i = 0;
thread t1([&i] {
while (i < 100)
{
if (i % 2)
{
cout << this_thread::get_id() << "->" << i << endl;
i++;
}
}
});
thread t2([&i] {
while (i <= 100)
{
if (i % 2 == 0)
{
cout << this_thread::get_id() << "->" << i << endl;
i++;
}
}
});
t1.join();
t2.join();
return 0;
}
第二个while加上=,虽然这可以做到要求,但是可能会造成资源浪费。可能有一种场景:当我们的t2满足条件正在运行,但是时间片到了,切换到t1,此时t1不满足条件,一直在while处死循环,直到时间片到了才切换出去,导致浪费占用CPU资源。所以我们希望两个线程能够相互通知,这就需要条件变量控制。
条件变量
条件变量的概念在线程同步——条件变量一文中我们介绍了
C++11也对条件变量进行了封装。头文件:
#include<condition_variable>
相关的接口:
条件变量不是线程安全的,要与锁互相配合使用。
int main()
{
int i = 0;
mutex mtx;
condition_variable cv;
thread t1([&] {
while (i < 100)
{
unique_lock<mutex> lock(mtx);
while (i % 2 == 0)//偶数
{
cv.wait(lock);
}
cout << "t1: " << this_thread::get_id() << "->" << i << endl;
i++;
cv.notify_one();
}
});
thread t2([&] {
while (i <= 100)
{
unique_lock<mutex> lock(mtx);
while (i % 2)
{
cv.wait(lock);
}
cout << "t2: " << this_thread::get_id() << "->" << i << endl;
i++;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}