C++ thread库

文章目录

C++11中提供了thread线程库,它本质上和pthread库差不多,只不过被封装了,同时它还是可以跨平台的

thread

构造函数

1
2
3
thread() noexcept;
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

注意

  1. 无参构造就只是开一个线程,但是不会工作,不会执行
  2. thread的第一个元素是可调用对象
  • lambda表达式
  • 函数指针
  • 类对象的仿函数
  1. 后面的是可变参数列表,可以传入任意的参数
  2. 第一个参数是一个模板参数,所以它是万能引用,既可以传左值也可以传右值

函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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表达式

1
2
3
4
5
int main()
{
thread t([](){cout<<"hello world"<<endl;});//使用lambda表达式进行传参
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}

仿函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Func
{

void operator()
{
cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
Func fc;
thread ss(fc);//使用类对象
thread s((Func()));//使用仿函数进行传参,就要这样弄,
}

使用类对象成员函数

  • 静态成员函数可以 直接使用对应的静态函数
  • 使用普通成员函数,除了 有成员函数还要有类对象 ,类对象可以是一个临时对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class test
{
public:
test()
{
}
~test()
{
}
static void do_work1()
{
cout << "do work 1" << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
void do_work2()
{
cout << "do work 2" << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
void do_work3(string& arg, int x, int y)
{
cout << "do work 3 x=" << x << "y=" << y << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};

void test_thread(int &data)
{
cout << "thread data=" << data << endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
}
int main()
{
int data=10;
thread t1(&test_thread,ref(data));
thread t2(&test::do_work1);//静态成员函数可以直接使用对应的静态函数
test t;
thread t3(&test::do_work2,t);//使用普通成员函数,除了有成员函数,还要有类对象
thread t3(&test::do_work3,t,std::ref("test"),10,20);//使用普通成员函数,除了有成员函数,还要有类对象


return 0;
}

拷贝构造

thread线程库不允许进行拷贝构造,所以直接把它给删除掉

1
thread (const thread&) = delete;

赋值重载

不允许赋值一个左值对象,类比拷贝构造
但是可以用一个右值对象来赋值

1
2
3
thread& operator= (thread&& rhs) noexcept;
copy [deleted] (2)
thread& operator= (const thread&) = delete;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
std::this_thread::get_id()
1
2
3
4
5
6
7
8
9
10
11
12
class Func
{

void operator()
{
cout << std::this_thread::get_id() << "++x " << x << endl; //获得对应的线程id,这里是一个结构体,因为它可以跨平台
}
int main()
{
thread s((Func()));//使用仿函数进行传参,就要这样弄,
}
};

sleep

1
std::this_thread::sleep_for(std::chrono::milliseconds(100));//这里面是休眠的时间

join和detach

  • join :就是在一个线程还没处理完之前,主线程都要一直等着这个线程做,直到新线程处理完了,才会放开主线程,
  • detach: 就是会把主线程和新线程分离开来,新线程的事情不影响主线程做事,后台自动回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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( ),之后就能用左值接收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

为了解决内置类型传参过去的线程安全的问题

1
atomic<T> s;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:如果这个锁已近被别人用了,就啥也不干直接返回,如果这个锁是空闲的,就把对应的线程给锁住
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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

1
2
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

wait后面的参数是可调用对象,同理,也是函数指针,lambda表达式,仿函数,当返回为true时,才会唤醒,否则一直阻塞着

notify_one:唤醒一个线程

实战
交替打印奇偶数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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;
}

C++ thread库
http://example.com/2022/08/20/C++ thread库/
作者
Zevin
发布于
2022年8月20日
许可协议