文章目录
C++11中提供了thread线程库,它本质上和pthread库差不多,只不过被封装了,同时它还是可以跨平台的
thread
构造函数
thread() noexcept;
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
注意
- 无参构造就只是开一个线程,但是不会工作,不会执行
- thread的第一个元素是可调用对象
- lambda表达式
- 函数指针
- 类对象的仿函数
- 后面的是可变参数列表,可以传入任意的参数
- 第一个参数是一个模板参数,所以它是万能引用,既可以传左值也可以传右值
函数指针
void print(int x,int y)//线程函数
{
cout<<x<<y<<endl;
}
void threads()
{
//线程库
thread t1;//这里可以创建一个无参的,即线程不执行
thread t2(print, 10,2);//第一个参数因为是模板,所以它是一个万能引用,func它不一定是一个函数,可调用对象就可以了,即可以传左值也可以传右值
//第二个参数是可变的模板参数,可以传0-n个参数
//第一个可以
t2.join();//这和c的线程库也是一样的
//不像c语言要使用一个结构体传进去,
//原来在c语言我们是使用线程id来控制这些线程的
}
Lambda表达式
int main()
{
thread t([](){
cout<<"hello world"<<endl;});//使用lambda表达式进行传参
}
int main()
{
int x = 0;
mutex mtx;
int N = 10000;
atomic<int> costtime1(0);
thread t1([&]
{
int begin1 = clock();
mtx.lock();
for (int i = 0; i < N; i++)
{
x++;
}
mtx.unlock();
cout<<x<<endl;
int end1 = clock();
costtime1 += (end1 - begin1);
cout<<costtime1<<endl;});//lambda表达式可以去处理一些小函数
cout << x << ":" << costtime1 << endl;
// costtime1就是调用花费的时间 }); //这里用一个可调用对象就可以了,我们这里用lambda表达式,&全部捕获 }); });
//项目里面,我们还是用原子的,相对更好一点
int costtime2 = 0;
thread t2([&]
{
int begin2 = clock();
mtx.lock();
for (int i = 0; i < N; i++)
{
x++;
}
mtx.unlock();
int end2 = clock();
costtime2 = end2 - begin2; }); // costtime1就是调用花费的时间 });
t1.join();
t2.join();
cout << x << ":" << costtime1 << endl;
return 0;
}
仿函数
class Func
{
void operator()
{
cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
Func fc;
thread ss(fc);//使用类对象
thread s((Func()));//使用仿函数进行传参,就要这样弄,
}
拷贝构造
thread线程库不允许进行拷贝构造,所以直接把它给删除掉
thread (const thread&) = delete;
赋值重载
不允许赋值一个左值对象,类比拷贝构造
但是可以用一个右值对象来赋值
thread& operator= (thread&& rhs) noexcept;
copy [deleted] (2)
thread& operator= (const thread&) = delete;
void test()
{
int n;
cin >> n;
vector<thread> vthds;
vthds.resize(n);//提前开好n个线程
//现在有任务来了,我们要让这些线程都跑起来
for (auto& e : vthds)
{
e = thread(print,100,2);//这里构造一个匿名对象赋值给它,这个地方又利用了一个移动赋值,把右边的这个临时
//对象传过去给e去执行,出了作用域就销毁了
//右边是一个右值
//这里的线程不支持拷贝构造,把一个线程拷贝给另一个线程,所以直接delete掉了
//线程也不支持赋值,但是可以支持移动赋值
}
}
获取id
std::this_thread::get_id()
class Func
{
void operator()
{
cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
thread s((Func()));//使用仿函数进行传参,就要这样弄,
}
};
sleep
std::this_thread::sleep_for(std::chrono::milliseconds(100));//这里面是休眠的时间
join和detach
- join :就是在一个线程还没处理完之前,主线程都要一直等着这个线程做,直到新线程处理完了,才会放开主线程,
- detach: 就是会把主线程和新线程分离开来,新线程的事情不影响主线程做事,后台自动回收
void print(int x,int y)//线程函数
{
cout<<x<<y<<endl;
}
int main()
{
thread s(print,10,20);
s.detach();
if(s.joinable())//判断是否可以被join,如果detach和join之后就不能被join
s.join();
cout<<"hello"<<endl;//使用join要等新线程处理完才会打印
return 0;
}
引用与传参
假如说我们在main函数里面定义了对象想要传到thread里面
- 可以使用指针进行传参
- 不能用左值来进行接收,但是可以使用std::ref( ),之后就能用左值接收
void func(int* x)//用指针肯定是可以的
{
*x+=10;
}
void func(int &x) //绝对不能传左值引用,但是下面传参是用std::ref()就能接收,因为正常thread里面都是拷贝
{
x += 10;
}
int main()
{
int n = 10;
// 严格来说thread的参数不能是左值引用,
thread t1(func,&n);//这样子对n的加,不可以,传值拷贝
thread t2(func, std::ref(n)); //这样弄就可以了
t1.join();
t2.join();
cout << n << endl;
}
atomic
为了解决内置类型传参过去的线程安全的问题
atomic<T> s;
atomic<int> x(0); //这样对x的操作就变成了原子操作,不能用=
atomic_long m{
0};//这两者是一样的
atomic<long> n(2);
void func()
{
x++;//因为这里的x是atomic原子变量,所以是线程安全的,
}
int main()
{
thread s(func);
thread p(func);
return 0;
}
void threadpool()
{
//实现一个线程池
atomic<int> x(0);
//我们实现一个n个线程都对它进行加m次
int n, m;
cin >> n >> m;
vector<thread> vthds;
vthds.resize(n); //我们直接就开n个线程,用thread的默认构造函数进行初始化,无参的,就不是不运行
//这里还有还可以用移动构造和移动赋值
atomic<int> costtime(0);
for (size_t i = 0; i < vthds.size(); i++)
{
vthds[i] = thread([m, &x, &costtime]()
{
int begin=clock();
for(int i=0;i<m;i++)
{
x++;//这里的x是原子变量
}
int end=clock();
costtime+=(end-begin); }); //这里我们用了移动赋值,构造了一个线程对象,线程里面用的是lambda表达式
}
for (auto &e : vthds)
{
if (e.joinable()) //判断是否可被join,
e.join(); //这里必须要用&,如果不用的话,就会去掉拷贝构造,这是不允许的
}
cout << x << endl;
cout << costtime << endl;
}
mutex
线程安全里面的锁资源
- lock就把临界区锁住了
- unlock可以把解锁
- try_lock:如果这个锁已近被别人用了,就啥也不干直接返回,如果这个锁是空闲的,就把对应的线程给锁住
int x = 0;
mutex mtx; //定义一个锁出来
void Func(int& n)
{
//每个线程都有自己的栈,各自在执行自己的func,
mtx.lock();
//不能放在里面,放在里面的话,每一次都要去竞争这个锁资源,
//加在外面变成了串行,运行,就没有意义了,理论上应该加在里面,这样就能交替并行运行
for (int i = 0; i < n; i++)
{
//放在这里锁的事情和释放也有消耗,
//对用户态的切换,要保存上下文
cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
//抢到锁的人执行的指令太少了,导致另一个人刚离开回去休息又回来了,而是在这里循环等待,一直问,好了我就进去执行,(自旋锁)
++x;
}
mtx.unlock();
}
int main()
{
int n=10;
thread th(Func,std::ref(n));
return 0;
}
lock_guard
我们使用锁会出现一种情况,一把锁锁住之后,但是里面就调用throw,抛异常之后,就会到catch里面,就把后续代码都跳过了,这个就会造成死锁的问题
所以我们就可以用一个RAII机制的锁,在调用的时候构造,上锁,在析构的时候解锁
lock_guard 只能在作用域结束后才能解锁
模拟实现lock_guard
template <class Lock>
class LockGuard
{
private:
Lock& _lock;//&,const,和没有默认构造函数的变量,都必须在初始化列表进行初始化
public:
LockGuard(Lock& lock)//在构造函数的时候就行加锁,但是互斥锁是不支持拷贝的,也要保持是同一把锁
:_lock(lock)//这里的_lock是mtx的别名
{
_lock.lock();
}
~LockGuard()
{
_lock.unlock();//在析构函数的时候进行解锁
}
};
unique_lock
和lock_guard类似,但是可以支持在作用域结束之前解锁
所以更加推荐使用unique_lock
void vfunc(vector<int> &vt, int x, int base, mutex &mtx)
{
try
{
/* code */
if (base == 200)
{
//对应第一个线程就让他sleep一下
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
for (int i = 0; i < x; i++)
{
//用IO把速度降下来
// mtx.lock(); //这样用锁有问题
// LockGuard<mutex> lock(mtx);//在这个里面就加锁,出了for作用域就解锁了,抛异常也算出了作用域,也解锁了,调用析构函数,生命周期到了
//lock_guard<mutex> lock(mtx);//这个是库里面提供的
unique_lock<mutex> lockk(mtx);//这个效果也是一样的,除了提供构造和析构,中途解一下锁
//这个push失败之后就会抛异常
vt.push_back(i); //有线程安全的问题
//抛异常之后unlock就不会被执行了,这样可能在上面push里面开空间也会出现问题,所以我们这里的锁可以写一个对象锁
if (base == 100 && i == 3)
throw bad_alloc();
//这里就死锁了,
// mtx.unlock();
//会出现死锁,在
}
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
//捕捉到异常之后,把锁释放掉
// mtx.unlock();
}
}
void test()
{
thread t1, t2;
vector<int> vt;
//两个线程要用同一个锁
mutex mtx;
//这里用的匿名对象,右值引用,线程要放在里面抛异常
t1 = thread(vfunc, std::ref(vt), 5, 100, std::ref(mtx)); //这样是存在线程安全问题
t2 = thread(vfunc, std::ref(vt), 10, 200, std::ref(mtx)); //
//这种小程序用lambda就行了
t1.join();
t2.join();
for (auto e : vt)
{
cout << e << " ";
}
}
cond_variable
wait
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
wait后面的参数是可调用对象,同理,也是函数指针,lambda表达式,仿函数,当返回为true时,才会唤醒,否则一直阻塞着
notify_one:唤醒一个线程
实战
交替打印奇偶数
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
//两个线程交替打印,一个打印奇数,一个打印偶数
void test1()
{
int n = 100;
// t1打印奇数
int i = 0;
mutex mtx;
bool flag = false;
condition_variable cv;
thread t1([&]()
{
while(i<n)
{
//尽量不要单独用lock和unlock
// lock_guard<mutex> lock(mtx);//这个是出了作用域才解锁
unique_lock<mutex> lock(mtx);
//wait后面的是可调用对象,函数,lambda,仿函数
// cv.wait(lock,[&flag](){return flag;});//在里面如果是false,就会一直阻塞,直到变成true才会开始,唤醒之后flag为true,就打印,
//这里的wait是直到条件为真才会去执行任务
cv.wait(lock,[&](){
return i%2==1;});
//唤醒和里面条件都会挡住它
cout<<this_thread::get_id()<<"->"<<i<<" "<<endl;
i++;
flag=!flag;
cv.notify_one();//唤醒一个
} });
// t2打印偶数
thread t2([&]()
{
while(i<n){
unique_lock<mutex> lock(mtx);
//!flag是true,这里获取到不会阻塞,就会运行了
// cv.wait(lock,[&flag](){return !flag;});
cv.wait(lock,[&](){
return i%2==0;});
cout<<this_thread::get_id()<<"->"<<i<<" "<<endl;;
i++;
flag=!flag;//保证下一个自己不会打印
cv.notify_one();//唤醒
} });
t1.join();
t2.join();
}
int main()
{
test1();
return 0;
}