文章目录
- 1、lambda
- 1、捕捉列表
- 2、简述C++线程
- 3、lambda对象大小
- 2、C++线程
- 1、整体了解
- 2、锁
- 1、互斥锁
- 2、递归互斥锁
- 3、时间控制锁
- 4、lock_guard
- 3、atomic(原子)
- 4、条件变量
1、lambda
在之前写排序时,用到过排升序,排降序,写了仿函数,有less和greater,这样就写好了排序。但是这样还是感觉有点复杂,因为我们还需要写两个函数。lambda可以解决这个问题,我们先看看lambda的语法。
int main()
{
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
return 0;
}
add1是一个lambda对象。上面也可以写成[](int x, int y)…(1, 2)。还可以写成这样
auto add2 = [](int x, int y)
{
return x + y;
};
lambda表达式之间不能互相赋值
1、捕捉列表
假如要交换两个变量的值
int x = 0, y = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(x, y);
cout << x << " " << y << endl;
//利用捕捉列表
auto swap2 = []()
{
int tmp = x;
x = y;
y = tmp;
};
swap2是无法用x和y,这相当于一个独立的域,只是定义在main里面,但不能访问main的变量。
auto swap2 = [x, y]() mutable
{
int tmp = x;
x = y;
y = tmp;
};
如果不写mutable,那么像这样写的就是传值捕捉,lambda内部是默认const修饰的,不能修改,加上异变mutable就是去掉这一属性,加上了mutable必须带上参数列表,即使为空。但这样写完后,值并没有交换,因为这是传值。把传值捕捉改成引用捕捉[&x, &y],这样不加mutable也可以交换值,不过这里就是一个坑点,&被识别为引用而不是取地址。
还可以混合捕捉,[&x, y]。全部引用捕捉[&],全部传值捕捉[=],[&, x]意思就是只有x是传值,其它都是引用,但不能这样写[&, =],因为意图不明确。
2、简述C++线程
我们不能像Linux那样写歌pthread_create来创建线程,因为这是POSIX
的线程库,是类Unix的系统用的,它是一个可移植的操作系统接口,Linux选择了这个标准,而Windows没有选择,Windows有自己的标准,所以Windows想要和Linux支持完全一样的线程代码,可以用条件编译
#ifdef _WIN32
CreateThread
#else
pthread_create
像这样来写,就麻烦。后来Linux,Windows都支持一个thread库,转换的操作由库来完成,这个库是一个面向对象的,库的用法和POSIX有很多相似之处,也有不一样的,比如函数参数args,C++11就用可变模板参数替代POSIX的args。
C++中线程头文件是<thread.h>。我们写一点lambda的代码来初步了解一下C++线程。
//传统写法
void Func(int n, int num)
{
for (int i = 0; i < n; ++i)
{
cout << num << ":" << i << " ";
}
cout << endl;
}
int main()
{
thread t1(Func, 7, 1);//一般不传仿函数,而是传函数指针
thread t2(Func, 4, 2);
//但是这里主线程提前结束了,就会崩,所以要join
t1.join();
t2.join();
return 0;
}
//lambda写法
int main()
{
int n1, n2;
cin >> n1 >> n2;
//thread t1(Func, 7, 1);//一般不传仿函数,而是传函数指针
//thread t2(Func, 4, 2);
//但是这里主线程提前结束了,就会崩,所以要join
thread t1([n1](int num)
{
for (int i = 0; i < n1; ++i)
{
cout << num << ":" << i << " ";
}
cout << endl;
}, 1);
thread t2([n2](int num)
{
for (int i = 0; i < n2; ++i)
{
cout << num << ":" << i << " ";
}
cout << endl;
}, 2);
t1.join();
t2.join();
return 0;
}
现在这两个线程代码有些冗余,因为它们做的操作都一样。线程不允许拷贝构造,但是可以移动构造。
int main()
{
size_t m;
cin >> m;
//要求m个线程分别打印n
vector<thread> vthds(m);
for (size_t i = 0; i < m; i++)
{
size_t n;
cin >> n;
vthds[i] = thread([i, n]() {
for (int j = 0; j < n; j++)
{
cout << i << ":" << j << endl;
}
cout << endl;
});
}
return 0;
}
给vthds[i]的是一个将亡值。如果要去掉这些线程,因为库中没有拷贝构造的原因,我们得用引用来删除。
for (auto& t : vthds)
{
t.join();
}
3、lambda对象大小
int main()
{
int x = 0, y = 1;
int m = 0, n = 1;
auto swap1 = [](int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
};
cout << sizeof(swap1) << endl;
return 0;
}
会打印出1。编译器会把lambda变成一个仿函数,并且又会把仿函数变成一个仿函数的类,lambda会被编译成一个仿函数类型的对象,仿函数的类是一个空类,没有给成员,所以它的大小是1。
在反汇编代码中,lambda会先构造一个仿函数对象,不过这个对象独特,每个lambda对象都不一样的仿函数类型,然后调用operator()函数。为什么两个lambda对象不能互相赋值?是因为它们的类型就不一样。
2、C++线程
1、整体了解
C++中线程的id是一个类,Linux是一个整数,获取id用get_id(),如果还在创建线程时就要看id,那么就用std里的子空间this_thread里的get_id函数来查看,哪个线程用就打印谁的id;Linux的sleep在C++中着两个函数,sleep_until和sleep_for,绝对和相对时间,时间上比如秒,second,休息多少秒是封装在一个chrono空间中的,这个需要引入头文件chrono
this_thread::sleep_for(chrono::seconds(1));
chrono里也有hours,minutes函数,milliseconds毫秒函数等。
无锁编程作为了解。无锁编程中会用yield接口,它可以主动让出时间片,也就是很少用锁的线程,实现无锁编程需要用到CAS,CAS能提供原子性的一系列操作,比如把旧值存起来,进行比较,如果在对这个值进行写操作的时候,它已经被改变了,那就放弃,进行swap或者set操作。在面对线程安全问题时,常用操作就是加锁,C++中也有锁的类,mutex。如果不加锁,那就是利用原子操作,它的本质就是CAS。
可以看陈皓大佬的这篇文章
无锁队列的实现
int i = 0;
i += 1;
CAS会如何处理这个+=?要对i加1,加1之前会先去i这个位置进行比较,旧值是0,新值是1,如果在写之前i的值和旧值一样,就set或者swap,不一样就false,不做操作,比较和set或swap是硬件上保证原子的。如果一个线程先改了值,另一个线程再过来时值已经被改动了,它会false,然后再继续判断它为什么false。
那么无锁编程如何利用CAS?在入队列时,先找到队列尾部,两个线程都想插入节点,如果tail的next是空,那就可以添加,那么另一个线程再去时就会发现tail的next不是空了,那它就退出,更新tail,再去看看能不能添加。
2、锁
1、互斥锁
经典场景
int x = 0;
void Func(int n)
{
for (int i = 0; i < n; i++)
{
++x;
}
}
int main()
{
thread t1(Func, 100);
thread t2(Func, 200);
t1.join();
t2.join();
return 0;
}
程序能够正常运行,是因为线程的栈是独立的,两个线程执行两个函数会在各自的栈中开辟栈帧,不过访问的全局变量是一样的。这里肯定有线程安全问题,毕竟访问的是同一个全局变量。为了解决安全问题就加锁。
#include <mutex>
int x = 0;
mutex mtx;
void Func(int n)
{
for (int i = 0; i < n; i++)
{
mtx.lock();
++x;
mtx.unlock();
}
}
int main()
{
thread t1(Func, 10000);
thread t2(Func, 20000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
这样写有问题,for循环应当在临界区内,进入临界区之前加锁。
void Func(int n)
{
//串行
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
//并行
/*for (int i = 0; i < n; i++)
{
mtx.lock();
++x;
mtx.unlock();
}*/
}
int main()
{
int n = 100000;
size_t begin = clock();
thread t1(Func, n);
thread t2(Func, n);
t1.join();
t2.join();
size_t end = clock();
cout << x << " | " << (end - begin) << endl;
return 0;
}
实际上它也会快很多。并行不一定就会串行快,还要看具体的场景。上面的并行场景,t1加锁了,然后在执行,t2看到了就去阻塞,带着上下文切出去,但是代码只有++x,t1走得很快,可能t2上下文还没切出去t1就结束了,t2又得回来。当执行的任务多了,并行就会比串行效率高了。
lambda写法
int main()
{
int n = 100000;
int x = 0;
mutex mtx;
size_t begin = clock();
thread t1([&, n](){
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
});
thread t2([&, n]() {
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx.unlock();
});
t1.join();
t2.join();
size_t end = clock();
cout << x << " | " << (end - begin) << endl;
return 0;
}
2、递归互斥锁
int x = 0;
void Func(int n)
{
if (n == 0)
return;
++x;
Func(n - 1);
}
int main()
{
thread t1(Func, 10000);
thread t2(Func, 10000);
t1.join();
t2.join();
cout << x << endl;
return 0;
}
Debug模式下会爆栈。加锁一下
void Func(int n)
{
if (n == 0)
return;
mtx.lock();
++x;
mtx.unlock();
Func(n - 1);
}
这样加锁可以运行,但是解锁放在Func(n - 1)后就不行了,造成了死锁问题,这里就需要用递归互斥锁,即使是放在Func(n - 1)后面也能运行,不过如果n过大还是会爆栈。递归互斥锁在进入递归时,如果发现有自己之前加的锁,那就不加锁了,即使有lock。
3、时间控制锁
timed_mutex类型,有try_lock_for try_lock_until try_lock等函数,具体的查看cplusplus.com,for接口会接收一个时间,如果过了这段时间还没解锁那就会直接解锁。还有递归的时间控制锁。
4、lock_guard
假设加锁后,临界区内抛出了异常,抛后会直接跳到捕获异常的地方,那就会造成死锁,因为没有解锁。这个的解决办法是写一个类,构造时加锁,析构时自动解锁
template <class T>
class LockGuard
{
public:
LockGuard(T& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
T _lk;
};
int x = 0;
mutex mtx;
void Func(int n)
{
for (int i = 0; i < n; i++)
{
try
{
//mtx.lock();
LockGuard<mutex> lock(mtx);
++x;
//模拟抛异常
if (rand() % 3 == 0)
{
throw exception("抛异常");
}
//mtx.unlock();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
异常之后会写。在抛异常时,锁出了作用域,就会自己解锁,也就是解决了死锁问题。但是这个写法是不对的
即使改成右值,初始化列表里写成_lk(move(lk))也不行,因为锁只有拷贝构造,没有移动构造。这样写就行
private:
T& _lk;
加引用的成员变量必须在初始化列表里初始化。
库中有这个类,不需要我们自己写,有两个lock_guard和unique_lock,头文件mutex。如果有可能抛异常的话,那就用这两个接口,unique_lock支持手动解锁。
3、atomic(原子)
C++11把它封装成库,支持CAS。它支持±*/与或异或的原子性。常用的是atomic的类,默认构造可以不给值,并且不允许拷贝,看一下之前的这个代码
int main()
{
int n = 100000;
int x = 0;
mutex mtx;
size_t begin = clock();
thread t1([&, n](){
mtx.lock();
for (int i = 0; i < n; i++)
{
++i;
}
mtx.unlock();
});
thread t2([&, n]() {
mtx.lock();
for (int i = 0; i < n; i++)
{
++i;
}
mtx.unlock();
});
t1.join();
t2.join();
size_t end = clock();
cout << x << " | " << (end - begin) << endl;
return 0;
}
改成原子的
int main()
{
int n = 100000;
atomic<int> x = 0;// = {0} or x{0}
//mutex mtx;
size_t begin = clock();
thread t1([&, n](){
//mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
//mtx.unlock();
});
thread t2([&, n]() {
//mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
//mtx.unlock();
});
t1.join();
t2.join();
size_t end = clock();
cout << x << " | " << (end - begin) << endl;
return 0;
}
底层是用CAS支持的。内置类型的变量要做一些简单的操作用原子类就好,不需要加锁。但是用原子类来定义的变量,传给参数不是原子类定义的函数,不能传,就需要用store或者load函数,x.load()和x.store()。
4、条件变量
头文件condition_variable,提供了两种接口,wait和notify
写两个线程交替打印数字,一个打印奇数,一个打印偶数。
int main()
{
int n = 100;
int x = 1;
size_t begin = clock();
thread t1([&, n](){
for (int i = 0; i < n; i++)
{
cout << this_thread::get_id() << ":" << x << endl;
}
});
thread t2([&, n]() {
for (int i = 0; i < n; i++)
{
cout << this_thread::get_id() << ":" << x << endl;
}
});
t1.join();
t2.join();
size_t end = clock();
cout << x << " | " << (end - begin) << endl;
return 0;
}
打印出来的结果并不是我们想象的那样的交替,有可能同时打印一个数,即使加锁也不能解决问题,这就得用到条件变量。条件变量要配合锁,锁会用unique_lock。
wait接口,当前线程会阻塞,直到被唤醒。wait时会修改条件变量的值,其它线程也有可能修改,所以在wait之前需要加锁;在wait发生的一瞬间,就会解锁,其它线程就可以使用锁了,后面notify的时候,唤醒线程,一瞬间就会加上锁,所以要用unique_lock,需要不靠类来解锁。wait有wait_for和wait_until。
notify有notify_one和notify_all,随机唤醒一个和唤醒所有,但是唤醒所有的话谁先占据资源就不确定,所以通常不使用all。 notify_one会唤醒正在等待条件的线程,没有等待的就什么都不做。
mutex mtx;
condition_variable cv;
int n = 100;
int x = 1;
size_t begin = clock();
thread t1([&, n](){
while (x < n)
{
unique_lock<mutex> lock(mutex);
cv.wait(lock);
cout << this_thread::get_id() << ":" << x << endl;
++x;
}
});
thread t2([&, n]() {
while(x < n)
{
unique_lock<mutex> lock(mutex);
cv.wait(lock);
cout << this_thread::get_id() << ":" << x << endl;
++x;
}
});
如何保证t1先运行,t2阻塞?wait会让线程阻塞在条件变量上。
thread t1([&, n](){
while (x < n)
{
unique_lock<mutex> lock(mutex);
cout << this_thread::get_id() << ":" << x << endl;
++x;
}
});
thread t2([&, n]() {
while(x < n)
{
unique_lock<mutex> lock(mutex);
cv.wait(lock);
cout << this_thread::get_id() << ":" << x << endl;
++x;
}
});
如果t1申请了锁,那就执行;如果t2申请了锁,然后去wait,释放锁,t1再申请到了锁,然后执行。等到t1执行完,t2才能申请到锁来执行代码。
那么接下来如何防止一个线程不断运行?如果t1这样写
thread t1([&, n](){
while (x < n)
{
unique_lock<mutex> lock(mtx);
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
线程都是要看自己的时间片来运行的,唤醒t2后,t2会去加锁,t1时间又回到了加锁那一行,因为t1时间片还没到,t1和t2抢锁,t1申请到了,t1就会继续打印。实际也确实会出现这种情况。
thread t1([&, n](){
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
{
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n]() {
while(x < n)
{
unique_lock<mutex> lock(mtx);
cv.wait(lock);
cout << this_thread::get_id() << ":" << x << endl;
++x;
}
});
按照这样写,t1连续得到锁后,它wait,释放锁,阻塞在条件变量上,t2就能拿到锁,然后执行,回到加锁代码行上,此时t1还在wait,没唤醒它,然后t2也来到wait那一行,两者就都wait,所以t2也要有唤醒的动作。但是即使有唤醒,也有可能两者运行速度不一样而导致问题,所以t2也得有条件判断。
thread t1([&, n](){
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
{
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n]() {
while(x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 != 0)
{
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
这样它们就都能保证交替打印了。
为什么会出现这些问题?t1拿到了锁,t2阻塞在锁上,t1打印,t1notify,但是没有线程wait,那么t1的锁出了作用域,解锁,这时候t1和t2竞争锁,一般情况下t2会获取锁,即使t1获取锁,它还是会wait。即使某个线程时间片到了,切出去了,另一个线程也不会继续打印。
也可以这样改
thread t1([&, n](){
while (1)
{
unique_lock<mutex> lock(mtx);
if (x >= n) break;
if (x % 2 == 0)//t1打印奇数,偶数就去阻塞
{
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
thread t2([&, n]() {
while(1)
{
unique_lock<mutex> lock(mtx);
if (x > n) break;
if (x % 2 != 0)
{
cv.wait(lock);
}
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
另外还有一个版本的wait,wait返回false就阻塞,true就继续执行,适合多生产多消费。
cv.wait(lock, [&x]() {return x % 2 != 0; });//t1
cv.wait(lock, [&x]() {return x % 2 == 0; });//t2
本篇gitee
结束。