[TOC]

关于多线程

​ 多线程可以同时做多件事,且可以互相交流影响

​ 首先包含头文件thread,例:

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
#include <iostream>
#include <thread>

static bool s_Finished = false;
void Dowork()
{
using namespace std::chrono_literals;
//注意C++14才添加了名称空间std::chrono_literals
//可用字面量 24h、30min、10s、2ms等
std::cout<<"started thread id = "<<std::this_thread::get_id()<<std::endl;
while (!s_Finished)
{
std::cout<< "working...\n";
std::this_thread::sleep_for(1s);
}
}

int main()
{
std::thread worker(Dowork);
//因为此处没有阻塞,所以开辟worker线程后主线程也开始往后执行!
std::cin.get();
//此处主线程阻塞等待回车键入
s_Finished = true;
//回车键入后,静态变量状态改变
//worker线程和主线程共用静态全局变量,状态改变,跳出循环!子线程结束!

worker.join();
//该语句是阻塞作用,等待worker线程结束主线程再执行下面语句!
std::cout<<"Finished."<<std::endl;
std::cout<<"started thread id = "<<std::this_thread::\
get_id()<<std::endl;
// \后直接接回车可实现换行,长代码实用trick
std::cin.get();
return 0;
/*输出:
started thread id = 140174567024384
working...
working...
working...

Finished.
started thread id = 140174584977216

*/
}

​ 若编译报链接错误——"对‘pthread_create’未定义的引用",则在g++编译时添加参数-lpthread或-pthread,或者在CMakeLists.txt中添加target_link_libraries(xout -lpthread)

关于互斥锁mutex

​ 为了实现线程之间的交流,需要用到互斥锁。做到在多个线程同时对一个内存空间操作的时候,保证线程安全。

​ 如果不加锁效果如下:

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
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;
class mutexTest {
public:
char _character;
mutexTest(char character):_character(character) {
}
void run() {
for (int i = 0; i < 10; i++) {
cout<<_character;
}
}
};
int main() {

mutexTest testClass1('*');
mutexTest testClass2('&');

thread* testThread1 = new thread(&mutexTest::run, &testClass1);//创建进程1
thread* testThread2 = new thread(&mutexTest::run, &testClass2);//创建进程2
//第一个参数是 函数指针,接收线程执行函数;第二个接收 该函数所在的 类对象指针
//两个线程不加锁,抢占标准输出设备!所以输出杂乱

testThread1->join();//等待进程1结束
testThread2->join();//等待进程2结束
cin.get();
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
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;
class mutexTest {
public:
char _character;
mutex* _mut;
mutexTest(char character):_character(character) {
}
void run() {
_mut->lock(); //要用标准输出的时候就加锁独享
for (int i = 0; i < 10; i++) {
cout<<_character;
}
_mut->unlock(); //用完释放锁
}
};
int main() {
mutex mut; //两个线程共用一把锁

mutexTest testClass1('*');
testClass1._mut = &mut;
//初始化线程一对象的锁,用的对象里的函数,所以该函数只能访问对象里的资源以及全局资源

mutexTest testClass2('&');
testClass2._mut = &mut; //初始化线程二对象的锁

thread* testThread1 = new thread(&mutexTest::run, &testClass1);//创建进程1
thread* testThread2 = new thread(&mutexTest::run, &testClass2);//创建进程2
//线程一先创建肯定先抢到锁,所以线程一先输出完(线程二阻塞等锁),释放锁后线程二抢到锁进行输出显示!

testThread1->join();//等待进程1结束
testThread2->join();//等待进程2结束
cin.get();
return 0;
/*输出:
**********&&&&&&&&&&
*/
}

关于unique_lock

unique_lock相当于作用域智能指针unique_ptr,unique_ptr利用局部变量生命周期出作用域即释放内存(利用析构函数),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
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;
class mutexTest {
public:
char _character;
mutex* _mut;
mutexTest(char character):_character(character) {
}
void run() {
unique_lock<mutex> lock(*_mut);
//智能锁,出作用域自动释放_mut
for (int i = 0; i < 10; i++) {
cout<<_character;
}
}
};
int main() {
mutex mut;
mutexTest testClass1('*');
testClass1._mut = &mut;
mutexTest testClass2('&');
testClass2._mut = &mut;
thread* testThread1 = new thread(&mutexTest::run, &testClass1);//创建进程1
thread* testThread2 = new thread(&mutexTest::run, &testClass2);//创建进程2

testThread1->join();//等待进程1结束
testThread2->join();//等待进程2结束
cin.get();
return 0;
}

关于并发

并发有两大需求,一是互斥,二是等待。互斥是因为线程间存在共享数据,等待则是因为线程间存在依赖

互斥的话,通过互斥锁能搞定,常见的有依赖操作系统的mutex

信号量条件变量,是为了解决等待需求。

​ 如考虑实现生产者消费者队列,生产者和消费者各是一个线程,线程间共享产品数据队列。一个明显的依赖是,消费者线程依赖生产者线程 push 元素进队列,然后才能消费产品。若不使用信号量、条件变量,则只能使用轮询(poll)的方式,循环查看队列是否为空,这种方式太过占用cpu资源。

信号量

​ 使用信号量解决进程间的依赖

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
//cmake目前不支持C++20,所以无法演示,但读懂代码意思就行
#include <thread>
#include <semaphore>
#include <queue>

extern std::semaphore sem; //****解决依赖问题
extern std::queue que;

void produce(Good good)
{
que.push(good);

sem.signal();
//增加商品计数值value,+1
//如果增加后value <= 0,说明之前有消费者睡眠了,则唤醒一个消费者
//如果value > 0 则说明消费者都没睡眠,正常消费无需唤醒
}

Good consume()
{
sem.wait();
//减少商品计数值value,-1
//如果减少后value < 0,说明目前队列没有商品,消费不了进入睡眠sleep
//有多个消费者的时候value可能为负数,代表等待的消费者个数的相反数

auto g = que.pop();
return g;
}

std::semaphore sem;
std::queue que;

int main()
{
std::thread Productor(produce);
std::thread Consumer(consume);

Productor.join();
Consumer.join();
}

信号量到互斥锁

​ 将信号量初始值设为1,解决共享内存互斥竞争问题,即一个时刻只有一个“人”访问这块共享内存,不然就数据混乱了。

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
//cmake目前不支持C++20,所以无法演示,但读懂代码意思就行
//信号量有统计计数,比较占资源,加锁解锁信号量的值都不超过1,故诞生了互斥锁!
#include <thread>
#include <semaphore>
//#include <mutex>
#include <queue>

extern std::semaphore sem;
extern std::semaphore mutex;
//extern std::mutex mutex;

extern std::queue que;

void produce(Good good)
{
mutex.wait();
//若生产者先抢到则value = value - 1 = 0,不睡眠
//此时若消费者再执行mutex.wait,value值变为-1,消费者进入睡眠
//(相当于互斥锁加锁)lock(mutex)

que.push(good);

mutex.signal();
//signal()增加value值,-1 + 1 = 0 <= 0,故唤醒消费者!
//(相当于互斥锁解锁)unlock(mutex)

sem.signal(); //****解决依赖
}

Good consume()
{
sem.wait(); //****解决依赖

mutex.wait(); //“加锁” lock(mutex)
auto g = que.pop();
mutex.signal(); //“解锁” unlock(mutex)

return g;
}

std::semaphore sem;
std::semaphore mutex;
//std::mutex mutex;
std::queue que;

int main()
{
std::thread Productor(produce);
std::thread Consumer(consume);

Productor.join();
Consumer.join();
}

条件变量

条件变量解决依赖问题。理解条件变量需要注意一点,条件变量自身并不包含条件判断,所以它通常和 if(或者while)一起使用,结合才叫条件变量。

​ 对于生产者消费者模型来说,如果只有一个消费者可以用 if ,但如果存在多个消费者则需要用 while 。因为当 broadcast 广播条件满足时,多个处于睡眠状态的消费者都在尝试加锁,其中某个首先加锁成功,将唯一的产品消费了,再解锁。这时第二个尝试加锁的消费者也加锁成功了,如果不进入循环再判断一次条件,直接去消费产品,会发现无产品可消费!所以需要用 while ,条件满足加锁后再进入循环判断一次条件,满足则跳出循环,不满足则释放锁进入睡眠。(循环时注意不要整个函数都锁着,不然别人没有空隙去抢锁。要保证锁的粒度尽量小!)。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <string>
#include <iostream>

extern std::condition_variable cond;
extern std::mutex mut;
extern std::queue<std::string> que;

void produce()
{
char good[256];
using namespace std::chrono_literals;
while (1)
{
std::this_thread::sleep_for(0.1s);
//***这里加锁前睡眠很重要,你不睡一会别人根本没机会加锁,没机会消费!!!
//mut.lock();
std::lock_guard<std::mutex> lk(mut);
//推荐使用lock_guard智能锁

std::cout<<"\n可以生产产品了!"<<std::endl;

std::cin.getline(good, 256);
que.push(good);


std::this_thread::sleep_for(0.4s);

//mut.unlock();

cond.notify_all();
//通知所以等待条件满足的消费者!
}
}

void consume()
{
std::string good;
using namespace std::chrono_literals;
while(1)
{
std::this_thread::sleep_for(3s);
//***这里加锁前长时间睡眠很重要!睡着的时候生产者可以加锁生产!!!
//***有条件变量的地方可以不睡!!没产品的时候(条件不满足)消费者不会一直去抢锁!
std::unique_lock<std::mutex> lk(mut);
while(que.empty())
cond.wait(lk);

std::cout<<"\n开始消费了!"<<std::endl;
good = que.front();

que.pop();

std::cout<<"产品是:"<< good << std::endl;
}
}

std::condition_variable cond;
std::mutex mut;
std::queue<std::string> que;

int main()
{
std::thread Productor(produce);
std::thread Consumer1(consume);
std::thread Consumer2(consume);

Productor.join();
Consumer1.join();
Consumer2.join();
/*输出:

可以生产产品了!
dad

可以生产产品了!
cdsc

开始消费了!
产品是:dad

开始消费了!
产品是:cdsc

可以生产产品了!
cdsdcs

开始消费了!
产品是:cdsdcs

可以生产产品了!}


*/
//输出解释:
//3s内快速生产两个,3s后两个一起消费;
//3s内只生产一个,3s后只有一个消费者有机会消费!
}

关于计时

​ 包含头文件chrono

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <chrono>
#include <thread>

int main()
{
using namespace std::chrono_literals;

auto start = std::chrono::high_resolution_clock::now();
//在编译器里可用auto接收,然后把鼠标放在变量上查看类型(并可复制)!
std::this_thread::sleep_for(1s);
auto end = std::chrono::high_resolution_clock::now();

std::chrono::duration<float> duration = end - start;
std::cout<< duration.count() << "s" <<std::endl;

std::cin.get();
return 0;
/*输出:
1.0008s

*/
}

​ 利用对象生存周期自动计时(推荐):

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
#include <iostream>
#include <chrono>
#include <thread>

struct Timer
{
std::chrono::_V2::system_clock::time_point start,end;
std::chrono::duration<float> duration;

Timer()
{
start = std::chrono::high_resolution_clock::now();
}
~Timer()
{
end = std::chrono::high_resolution_clock::now();
duration = end - start;

float ms = duration.count() * 1000.0f;
//转成毫秒
std::cout << "Timer took: " << ms << "ms "<<std::endl;
}
};

void funtion()
{
Timer timer;
//利用函数生命周期自动给函数计时!
for(int i = 0; i<100; i++);
std::cout<<"Hello"<<std::endl;
}
int main()
{
using namespace std::chrono_literals;

{
Timer timer;
std::this_thread::sleep_for(1s);
} //利用局部变量生命周期自动计时!

funtion();

std::cin.get();
return 0;
/*输出:
Timer took: 1000.9ms
Hello
Timer took: 0.003767ms

*/
}