《重学C++》9. C++进阶编程(三)多线程

多线程基础

早期C++不支持,Win下只能使用微软提供的API。C++11之后在<thread>头文件中提供了Thread类,可以实现多线程。

线程创建和终止:

1
2
3
pthread_create (thread, attr, start_routine, arg) //线程创建(全参数)
pthread_create (start_routine, arg) //线程创建(部分参数),一般使用这种方式即可
pthread_exit (status) //线程终止
参数 描述
threa 指向线程标识符指针。
attr 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
start_routine 线程运行函数起始地址,一旦线程被创建就会执行。
arg 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。

C++ 多线程|菜鸟教程
https://www.runoob.com/cplusplus/cpp-multithreading.html
thread - C++ reference
http://www.cplusplus.com/reference/thread/thread/?kw=thread
C++11多线程并发基础入门教程
https://zhuanlan.zhihu.com/p/194198073?utm_source=com.tencent.wework

进程创建方式的区别

1
2
3
std::thread my_thread(background_task());  //方式一
std::thread my_thread((background_task())); //方式二
std::thread my_thread{background_task()}; //方式三

方式一声明了一个叫做my_thread的函数,其接收一个指针作为参数(这个指针指向一个函数,该函数无参数,返回一个background_task对象)然后返回一个std::thread对象,并不是创建了一个新的thread。通过方式二和方式三可以避免这样。
方式二多加了一对小括号,这个额外的括号避免了这个语句被翻译成一个函数声明。因此方式二声明了一个std::thread类型的my_thread变量。
方式三使用大括号而不是小括号,通过这种新的语法,同样声明了一个变量。(和方式二相同)

c++ 中 std::thread 调用仿函数问题? - 一只特立独行的猪的回答 - 知乎
https://www.zhihu.com/question/263965095/answer/274998573

join函数

该函数执行的前提是该线程joinable() == true(该线程可以被join)。
官方描述如下:

The function returns when the thread execution has completed.
当线程执行完成时,函数返回。
This synchronizes the moment this function returns with the completion of all the operations in the thread: This blocks the execution of the thread that calls this function until the function called on construction returns (if it hasn’t yet).
这将在函数返回的时刻与线程中所有操作的完成同步:这将阻塞调用该函数的线程的执行,直到被调用的构造函数返回(如果它还没有返回)。

1
2
3
4
5
6
7
8
9
int main()
{
//若使用仿函数,一定要使用方式二(加小括号)/方式三(使用大括号),不能写thread t(hello());
//如果是普通函数,可以写thread t(hello);
thread t((hello())); //等价于thread t{hello()};
t.join();
//t线程结束后,这里的代码才能被执行
return 0;
}

以上面代码为例,简单来说就是,当main函数执行到thread t((hello()));时,两个线程开始同步运行。为了防止调用t的主函数main线程执行完的时候线程t还没有结束,因此需要让main函数的线程等待t线程执行结束(通过join函数让线程等待),才可以继续执行后面的代码。
由于多个线程并行执行任务,带来的一个问题是如何解决多线程执行进度不同步的问题。就像写书,一个人写不需要协调,但是多个人合作写书时,写书进度有的快有的慢,进度不一致会带来问题。

mutex互斥锁

互斥锁是C++中锁的一种,定义在头文件中。
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中有多个空闲线程和一个任务队列。任何一个线程都要通过使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列,导致发生错乱。

在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。生活中一个例子就是上厕所,一间厕所最多只能一个人上(一个资源可以不被使用,但最多只能同时被一个人用),轮到上厕所的人刚进入厕所第一件事是上锁,防止别人进入、然后是解手(线程中就是进行操作)、最后一件事是释放该锁,离开。之后其他人可以上厕所(使用该资源),但轮到谁使用与自己无关(由操作系统决定)。

C++线程中的几种锁
https://blog.csdn.net/bian_qing_quan11/article/details/73734157
C++11线程中的几种锁
https://blog.csdn.net/xy_cpp/article/details/81910513

示例一:

这个示例比较简单、基础,涉及线程创建(有参无参两种)、cout共享变量和锁

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;
mutex tex;

void T1() {
tex.lock();
cout << "T1 Hello World!" << endl;
tex.unlock();

return;
}

//加上const,一是防止对字符串变量修改
//二是如果实参是字符串常量,只有加上const才能运行
void T2(const char *str) {
tex.lock();
cout << "T2 " << str << endl;
tex.unlock();
return;
}

int main()
{
thread t1(T1);
thread t2(T2, "Hello World"); //形参只有加上const才能运行
t1.join();
t2.join();
cout << "Hi main"<< endl;

return 0;
}

如果不进行上锁,可能会出现t1线程输出一半,然后t2线程输出(打断t1)的问题。上面的上锁实际上是实现了对共享资源——标准输出cout——的互斥访问,也让两个进程实现了原子性(要么不做,要么把所有操作做完)。

示例二

多线程实现银行存取款。

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;

void deposit(mutex &tex,int &money) {
for (int i = 0; i < 100; i++)
{
tex.lock();
money += 1;
tex.unlock();
}
return;
}


void withdraw(mutex &tex, int &money) {
for (int i = 0; i < 100; i++)
{
tex.lock();
money -= 1;
tex.unlock();
}
return;
}

int main()
{
mutex money_mutex;
int money = 2000;

cout << money << endl;
thread t1(deposit,ref(money_mutex), ref(money) );
thread t2(withdraw, ref(money_mutex), ref(money));
t1.join();
t2.join();
cout << money << endl;

return 0;
}

有几点需要注意:

  • 尽量少使用全局变量,可以降低变量被修改的风险。
  • 锁的粒度最小化,该用锁的地方使用,不该用的时候不用,避免不必要的加锁。这样可以尽量实现异步,达到更高运行效率。
  • thread编程应当注意参数的传递方式(因为函数参数是值拷贝),有时候需要引用,而且thread编程中引用是“ref()”而不是“&”。

线程和进程、同步和异步

线程类似于容器,是分配资源的最小单位;线程是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
thread tW1([](){
cout<<"ThreadSwap1"<<endl;
});
thread tW2([](){
cout<<"ThreadSwap2"<<endl;
});
cout<<"ThreadSwap1 id is"<<tW1.get_id()<<endl;
cout<<"ThreadSwap2 id is"<<tW2.get_id()<<endl;

cout<<"Swap after:"<<endl;
//等价于tW1.swap(tW2);
swap(tW1,tW2); //线程交换(句柄之间发生了交换)
cout<<"ThreadSwap1 id is"<<tW1.get_id()<<endl;
cout<<"ThreadSwap2 id is"<<tW2.get_id()<<endl;

t1.join();
t2.join();

//线程移动
thread.tM1([](){cout<<"ThreadSwap1"<<endl;});
//tM1.join();
cout<<"ThreadSwap1 id is"<<tM1.get_id()<<endl;
cout<<"Swap after"<<endl;
thread tM2 = move(tM1);
cout<<"ThreadSwap2 id is"<<tM2.get_id()<<endl;
tM2.join();

public member function std::thread::swap
void swap (thread& x) noexcept;
Swaps the state of the object with that of x.
和对象x交换状态

没有搞懂线程的交换、移动有什么作用,虽然说了是交换状态,但没弄清楚具体交换、移动了哪些东西(不过至少thread id肯定是交换/移动了)。
看了下swap函数的代码,猜测应该是交换了线程的资源和函数等所有内容。