《C++并发编程实战》笔记

书籍中文版翻译地址

书中源码地址

一、你好,C++的并发世界!

1、最简单和最基本的并发,是指两个或更多个独立的活动同时发生。

2、为性能而使用并发就像其他所有优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能使代码复杂化,使其更难理解,并更容易出错。

3、C++多线程历史:C++98标准不承认线程的存在,并且各种语言要素的操作效果都已顺序抽象机的形式编写。不仅如此,内存模型也没有正式定义,所以没办法在缺少编译器相关扩展的情况下编写多线程应用程序。但C++11新标准改变了这一切,主要模型是Boost中的线程库。

4、管理线程的函数和类在头文件中,而保护共享数据的函数和类在其他头文件中声明。简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<thread>//添加头文件
using namespace std;

void hello()//每个线程都必须有一个初始函数,新线程的执行将从这里开始,对于主线程来说是main()。
{
cout << "Hello World!" << endl;
}

int main()
{
thread t(hello);//将hello函数作为新线程的初始函数,
t.join();//使调用线程等待t相关联的线程运行结束,少了这一行会报错,因为main可能就会在新线程运行之前结束了。
}

二、线程管理

1、使用C++线程库启动线程,可以归结为构造thread对象。

2、构造对象时要避免“最令人头痛的语法解析”问题,如:
std::thread my_thread(background_task());
这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参 数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。
可使用如下写法避免:
std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2
或用lambda表达式:
std::thread my_thread([]{
​ do_something();
​ do_something_else(); })

3、启动了线程,你需要明确是要等待线程结束(加入式),还是让其自主运行(分离式)。

4、使用一个能访问局部变量的函数去创建线程是一个糟糕的主意(除非十分确定线程会在函数完成前结束)。

5、通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何用户接口,并且在后台运行的线程。

6、向线程函数函数传递参数。P29

7、需要在线程对象被析构前,显式地等待线程完成,或者分离它;进行复制时也需要满足这些条件(说明:不能通过赋一个新值给std::thread对象的方式来”丢弃”一个线程)。

8、 std::thread::hardware_concurrency()在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数。

9、线程标识类型是std::thread::id,可以通过两种方式进行检索。第一种,可以通过调 用std::thread 对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值,这个值表示“没有线程”。第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可以获得线程标识。

三、线程间共享数据

1、不变量通常会在一次更新中被破坏,特别是比较复杂的数据结构,或者一次更新就要改动很大的数据结构。

2、C++中通过实例化std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。C++标准库为互斥量提供了一个RAII语法的模板类std::lack_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。

3、具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被互斥锁限制。互斥量保护的数据需要对接口的设计相当谨慎,要确保互斥量能锁住任何对保护数据的访问,并且不留后门