C++ 并行编程

news/2024/5/2 8:31:33/文章来源:https://blog.csdn.net/qq_28087491/article/details/127464635

C++ 并行编程

  • 1. 进程和线程
    • 1.1 常规解释
    • 1.2 总结
    • 1.3 具体理解
    • 1.4 为什么使用多线程
    • 1.5 进程和线程的区别
  • 2. 并发与并行
    • 2.1 多进程并发
    • 2.2 多线程并发
  • 3. C++中的多线程
  • 4. 时间管理
    • 4.1 C语言:time.h
    • 4.2 C++11时间标准库:std::chrono
      • 4.2.1 获取时间段 int64_t/double
      • 4.2.2 跨平台的 sleep: std::this_thread::sleep_for
      • 4.2.3 睡到时间点: std::this_thread::sleep_until
  • 5. 线程
    • 5.1 为什么需要多线程:无阻塞多任务
    • 5.2 现代 C++ 中的多线程:std::thread
    • 5.3 主线程等待子线程结束:t1.join()
    • 5.4 析构函数不再销毁线程:t1.detach()
    • 5.5 解构函数不再销毁线程:移动到全局线程池
    • 5.6 C++20 std::jthread:符合 RAII 思想,析构时自动 join()
    • 5.7 joinable()
  • 6. 异步
    • 6.1 异步好帮手:std::async
    • 6.2 显式地等待:wait()
    • 6.3 等待一段时间:wait_for()
    • 6.4 另一种用法:std::launch::deferred 做参数
    • 6.5 std::async 的底层实现:std::promise
    • 6.6 std::future 小贴士
  • 7. 互斥量
    • 7.1 std::mutex:上锁,防止多个线程同时进入某一代码段- **std::promise**:Promise
    • 7.2 std::lock_guard:符合 RAII 思想的上锁和解锁
    • 7.3 std::unique_lock:也符合 RAII 思想,但自由度更高
      • 7.3.1 std::unique_lock:用 std::defer_lock 作为参数
    • 7.4 多个对象?每个对象一个 mutex 即可
    • 7.5 如果上锁失败,不要等待:try_lock()
    • 7.6 只等待一段时间:try_lock_for()
    • 7.7 std::unique_lock补充
      • 7.7.1 用 std::try_to_lock 做参数
      • 7.7.2 用 std::adopt_lock 做参数
    • 7.8 std::unique_lock 和 std::mutex 具有同样的接口
  • 8. 死锁
    • 8.1 解决方案
      • 8.1.1 永远不要同时持有两个锁
      • 8.1.2 保证双方上锁顺序一致
      • 8.1.3 用 std::lock 同时对多个上锁
      • 8.1.4 std::lock 的 RAII 版本:std::scoped_lock
    • 8.2 同一个线程重复调用 lock() 也会造成死锁
      • 8.2.1 解决1:other 里不要再上锁
      • 8.2.2 改用 std::recursive_mutex
  • 9. 数据结构
    • 9.1 封装一个线程安全的 vector
      • 9.1.1 逻辑上 const 而部分成员非 const:mutable
    • 9.2 为什么需要读写锁?
      • 9.2.1 读写锁:shared_mutex
      • 9.2.2 std::shared_lock:符合 RAII 思想的 lock_shared()
      • 9.2.3 只需一次性上锁,且符合 RAII 思想:访问者模式
  • 10. 条件变量
    • 10.1 条件变量:等待被唤醒
    • 10.2 条件变量:等待某一条件成真
    • 10.3 条件变量:多个等待者
      • 10.3.1 案例:实现生产者-消费者模式
      • 10.3.2 条件变量:将 foods 队列封装成类
    • 10.4 注意事项
  • 11. 原子操作
    • 11.1 经典案例:多个线程修改同一个计数器
    • 11.2 暴力解决:用 mutex 上锁
    • 11.3 建议用 atomic:有专门的硬件指令加持
      • 11.3.1 注意:请用 +=,不要让 + 和 = 分开
      • 11.3.2 调用函数名
      • 11.3.3 fetch_add:会返回其旧值
      • 11.3.4 exchange:读取的同时写入
      • 11.3.5 compare_exchange_strong:读取,比较是否相等,相等则写入
      • 11.3.6 方便理解的伪代码

Reference:

  1. C++并行编程
  2. 进程线程(一)——基础知识,什么是进程?什么是线程?
  3. Reference <thread> 包含每个API的详细用法
  4. 【公开课】C++11开始的多线程编程(#5)文章的主要来源,建议看原视频

1. 进程和线程

1.1 常规解释

进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

1.2 总结

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。是程序执行的最小单位。

1.3 具体理解

Linux环境下,每个进程有自己各自独立的 4G 地址空间,大家互不干扰对方,如果两个进程之间通信的话,还需要借助第三方进程间通信工具 IPC 才能完成。不同的进程通过页表映射,映射到物理内存上各自独立的存储空间,在操作系统的调度下,分别轮流占用CPU去运行,互不干扰、互不影响,甚至相互都不知道对方。在每个进程的眼里,CPU就是他的整个世界,虽然不停地被睡眠,但是一旦恢复运行,一觉醒来,仿佛什么都没发生过一样,认为自己拥有整个CPU,一直在占有它。

在一个进程中,可能存在多个线程,每个线程类似于合租的每个租客,除了自己的私有空间外,还跟其它线程共享进程的很多资源,如地址空间、全局数据、代码段、打开的文件等等。在线程中,通过各种加锁解锁的同步机制,一样可以用来防止多个线程访问共享资源产生冲突,比如互斥锁、条件变量、读写锁等。

进程具有的特征:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
  • 并发性:任何进程都可以同其他进行一起并发执行;
  • 独立性:进程是系统进行资源分配和调度的一个独立单位;
  • 结构性:进程由程序,数据和进程控制块三部分组成

对于操作系统来说,它可以同时运行多个任务。你可以一边听歌,一边打游戏,一边还等着QQ开着语音聊着天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。对于过去的单核CPU,也可以完成这些任务,由于CPU执行代码都是顺序执行的,那么,单核CPU就轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

1.4 为什么使用多线程

  1. 和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统中,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护其代码段、堆栈段和数据段,这种多任务工作方式的代价非常“昂贵”。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且线程间彼此切换所需要时间也远远小于进程间切换所需要的时间。

  2. 线程间方便的通信机制。对不同进程来说它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行。这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,不仅方便,而且快捷。

1.5 进程和线程的区别

  1. 什么是进程,什么是线程?
    • 进程是程序一次执行的过程,动态的,进程切换时系统开销大
    • 线程是轻量级进程,切换效率高
  2. 进程和线程的空间分配?
    • 每个进程都有独立的0-3G的空间,都参与内核调度,互不影响
    • 同一进程中的线程共享相同的地址空间(共享0-3G)
  3. 进程之间和线程之间各自的通信方式
    • 进程间:(7种)无名管道、有名管道、信号机制、信号灯、共享内存、消息队列、套接字socket
    • 线程间:全局变量,信号量,互斥锁

2. 并发与并行

并发同一时间段内可以交替处理多个操作
并行同一时间段内可以同时处理多个操作
如果程序的结构设计为可以并发执行的,那么在支持并行的机器上,程序可以并行地执行。

2.1 多进程并发

多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。

2.2 多线程并发

在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。

3. C++中的多线程

C++11标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作(atomic operation)等各种类。

<thread>: 包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在该头文件中有声明;
<atomic>: 包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数;
<mutex>: 包含了与互斥量相关的类以及其他类型的函数;
<future>: 包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数;
<condition_variable>: 包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any

在这里插入图片描述

那么现在开始吧,看看C++11开始的多线程编程是如何实现的!


4. 时间管理

4.1 C语言:time.h

long t0 = time(NULL);    // 获取从1970年1月1日到当前时经过的秒数
sleep(3);                        // 让程序休眠3秒
long t1 = t0 + 3;             // 当前时间的三秒后
usleep(3000000);          // 让程序休眠3000000微秒,也就是3秒

在 C语言中,使用 time(NULL) 获取当前时间,返回的是一个整数long。
其中,使用 sleep() 是让程序休息整数秒;而如果想让程序休息微秒,需使用 usleep()。
这样可以看出,C 语言原始的 API 内,没有类型区分,导致很容易弄错单位,混淆时间点时间段

4.2 C++11时间标准库:std::chrono

因此,从 C++11 开始,就将时间标准化了,它利用 C++ 强类型的特点,明确区分时间点时间段,明确区分不同的时间单位

  • 时间点例子:2022年1月8日 13点07分10秒
  • 时间段例子:1分30秒
  • 时间点类型:chrono::steady_clock::time_point 等
  • 时间段类型:chrono::milliseconds,chrono::seconds,chrono::minutes 等
  • 方便的运算符重载:时间点+时间段=时间点,时间点-时间点=时间段

4.2.1 获取时间段 int64_t/double

auto t0 = chrono::steady_clock::now();// 获取当前时间点
auto t1 = t0 + chrono::seconds(30);// 当前时间点的30秒后
auto dt = t1 - t0;// 获取两个时间点的差(时间段)
int64_t sec = chrono::duration_cast<chrono::seconds>(dt).count();// 时间差的秒数

举个例子,计算一个步骤花费时间:

#include <iostream>
#include <chrono>int main() {auto t0 = std::chrono::steady_clock::now();for (volatile int i = 0; i < 10000000; i++);auto t1 = std::chrono::steady_clock::now();auto dt = t1 - t0;int64_t ms = std::chrono::duration_cast<std::chrono::milliseconds>(dt).count();std::cout << "time elapsed: " << ms << " ms" << std::endl;return 0;
}

其中

typedef std::chrono::duration<int64_t, std::milli> std::chrono::milliseconds。

在例子中返回的是一个整数的毫秒数,如果想让精度超过毫秒,可以使用:

using double_ms = std::chrono::duration<double, std::milli>;

程序如下:

int main() {auto t0 = std::chrono::steady_clock::now();for (volatile int i = 0; i < 10000000; i++);auto t1 = std::chrono::steady_clock::now();auto dt = t1 - t0;using double_ms = std::chrono::duration<double, std::milli>;double ms = std::chrono::duration_cast<double_ms>(dt).count();std::cout << "time elapsed: " << ms << " ms" << std::endl;return 0;
}

原理为:
duration_cast 可以在任意的 duration 类型之间转换。
duration<T, R> 表示用 T 类型表示,且时间单位是 R
R 省略不写就是秒,std::milli 就是毫秒,std::micro 就是微秒
seconds 是 duration<int64_t> 的类型别名;milliseconds 是 duration<int64_t, std::milli> 的类型别名
这里我们创建了 double_ms 作为 duration<double, std::milli> 的别名

上面程序的结果可见下图所示,这样得到的结果就有小数点了。
在这里插入图片描述

4.2.2 跨平台的 sleep: std::this_thread::sleep_for

以前睡眠一段时间,不同操作系统使用不同的API。在 C++11 中可以用 std::this_thread::sleep_for 替代 Unix 类操作系统专有的 usleep。他可以让当前线程休眠一段时间,然后继续。(睡眠一个时间段)

这个 API 单位也可以自己指定,比如在下面示例中,使用 milliseconds 表示毫秒,也可以换成 microseconds 表示微秒,seconds 表示秒,chrono 的强类型让单位选择更自由。

int main() {std::this_thread::sleep_for(std::chrono::milliseconds(400));return 0;
}

4.2.3 睡到时间点: std::this_thread::sleep_until

除了接受一个时间段的 sleep_for,还有接受一个时间点的 sleep_until,表示让当前线程休眠直到某个时间点。(睡眠到一个时间点)

在下面这个例程中,与 4.2.2 节中直接睡眠 400ms 是等价的。

int main() {auto t = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);std::this_thread::sleep_until(t);return 0;
}

5. 线程

  • <thread>
    Header that declares the thread class and the this_thread namespace.

5.1 为什么需要多线程:无阻塞多任务

我们的一个独立程序常常需要同时处理多个任务。例如:后台在执行一个很耗时的任务,比如下载一个文件,同时还要和用户交互。这在 GUI 应用程序中很常见,比如浏览器在后台下载文件的同时,用户仍然可以用鼠标操作其 UI 界面。

现在来看下面例子:

#include <iostream>
#include <thread>
#include <string>void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}int main() {download("hello.zip");interact();return 0;
}

在这个程序内,如果没有多线程,就必须等文件下载完了才能继续和用户Say Hi。下载完成前,整个界面都会处于“未响应”状态,用户想做别的事情就做不了。
在这里插入图片描述

5.2 现代 C++ 中的多线程:std::thread

老版本的 C 语言有 库,而从 C++11 开始,为多线程提供了语言级别的支持。他用 std::thread 这个类来表示线程。std::thread 构造函数的参数可以是任意 lambda 表达式。在这个线程启动时,就会执行 lambda 里的内容。这样就可以一边和用户交互,一边在另一个线程里慢吞吞下载文件了。

void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}int main() {std::thread t1([&] {download("hello.zip");});interact();return 0;
}

当直接编译这个代码时,会发现在链接时会出现问题。这是因为 std::thread 的实现背后是基于 pthread 的。需要在 CMakeLists.txt 里链接 Threads::Threads

cmake_minimum_required(VERSION 3.10)set(CMAKE_CXX_STANDARD 17)project(cpptest LANGUAGES CXX)add_executable(cpptest main.cpp)find_package(Threads REQUIRED)
target_link_libraries(cpptest PUBLIC Threads::Threads)

5.3 主线程等待子线程结束:t1.join()

  • std::thread::join: Join thread
    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).
    After a call to this function, the thread object becomes non-joinable and can be destroyed safely.

现在已经有多线程了,文件下载用户交互分别在两个线程,同时独立运行。从而下载过程中也可以响应用户请求,提升了体验。

这时运行上面的程序,会发现一个问题:在输入完 ling 以后,程序的确及时地和我交互了。但是用户交互所在的主线程退出后,文件下载所在的子线程,因为从属于这个主线程,也被迫退出了。
在这里插入图片描述
因此,我们想要让主线程不要急着退出,等子线程也结束了再退出。可以用 std::thread 类的成员函数 join() 来等待刚刚创建的t1线程结束。

int main() {std::thread t1([&] {download("hello.zip");});interact();std::cout << "Waiting for child thread..." << std::endl;t1.join();std::cout << "Child thread exited!" << std::endl;return 0;
}

5.4 析构函数不再销毁线程:t1.detach()

  • std::thread::detach:Detach thread
    Detaches the thread represented by the object from the calling thread, allowing them to execute independently from each other.
    Both threads continue without blocking nor synchronizing in any way. Note that when either one ends execution, its resources are released.
    After a call to this function, the thread object becomes non-joinable and can be destroyed safely.

现在看下面这个例子:

void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}void myfunc() {std::thread t1([&] {download("hello.zip");});// 退出函数体时,会销毁 t1 线程的句柄!
}int main() {myfunc();interact();return 0;
}

作为一个 C++ 类,std::thread 同样遵循 RAII 思想(RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象)和三五法则:因为管理着资源,他自定义了析构函数,删除了拷贝构造函数,但是提供了移动构造函数。因此,当 t1 所在的函数 myfunc() 退出时,就会调用 std::thread 的析构函数,这会销毁 t1 线程。比如说在上面的例程中就会报以下错误:
在这里插入图片描述

这个时候就可以调用成员函数 detach() 分离该线程——意味着线程的生命周期不再由当前 std::thread 对象管理,而是在线程退出以后自动销毁自己

void myfunc() {std::thread t1([&] {download("hello.zip");});t1.detach();// t1 所代表的线程被分离了,不再随 t1 对象销毁
}

不过这样写还是有一个bug,就是之前说的,还是会在进程退出时候自动退出。(所以说,join()detach() 是有显著区别的。)
在这里插入图片描述

5.5 解构函数不再销毁线程:移动到全局线程池

也就是说,detach() 的问题是进程退出时候不会等待所有子线程执行完毕。所以另一种解法是把 t1 对象移动到一个全局变量去,从而延长其生命周期到 myfunc 函数体外。这样就可以等下载完再退出了。

void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}std::vector<std::thread> pool;void myfunc() {std::thread t1([&] {download("hello.zip");});// 移交控制权到全局的 pool 列表,以延长 t1 的生命周期pool.push_back(std::move(t1));
}int main() {myfunc();interact();for (auto &t: pool) t.join();  // 等待池里的线程全部执行完毕return 0;
}

但是需要在 main 里面手动 join 全部线程还是有点麻烦,我们可以自定义一个类 ThreadPool,并用他创建一个全局变量,其解构函数会在 main 退出后自动调用。(毕竟这里创建的是一个全局变量)

void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}class ThreadPool {std::vector<std::thread> m_pool;public:void push_back(std::thread thr) {m_pool.push_back(std::move(thr));}~ThreadPool() {                      // main 函数退出后会自动调用for (auto &t: m_pool) t.join();  // 等待池里的线程全部执行完毕}
};ThreadPool tpool;void myfunc() {std::thread t1([&] {download("hello.zip");});// 移交控制权到全局的 pool 列表,以延长 t1 的生命周期tpool.push_back(std::move(t1));
}int main() {myfunc();interact();return 0;
}

5.6 C++20 std::jthread:符合 RAII 思想,析构时自动 join()

但是在上一节的内容中,还是需要自定义析构函数。C++20 引入了 std::jthread 类(注意是20才有的新特性),和 std::thread 不同在于:它的析构函数里会自动调用 join() 函数,从而保证 pool 析构时会自动等待全部线程执行完毕。(注意CMakeLists.txt内C++标准要修改为set(CMAKE_CXX_STANDARD 20))

#include <iostream>
#include <thread>
#include <string>
#include <vector>void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}// ~jthread() 解构函数里会自动调用 join(),如果 joinable() 的话
std::vector<std::jthread> pool;void myfunc() {std::jthread t1([&] {download("hello.zip");});// 移交控制权到全局的 pool 列表,以延长 t1 的生命周期pool.push_back(std::move(t1));
}int main() {myfunc();interact();return 0;
}

5.7 joinable()

  • std::thread::joinable:Check if joinable
    Returns whether the thread object is joinable.
    A thread object is joinable if it represents a thread of execution.
    A thread object is not joinable in any of these cases:
    • if it was default-constructed.(如下面例子中的 std::thread foo; thread 对象不允许默认构造函数)
    • if it has been moved from (either constructing another thread object, or assigning to it).
    • if either of its members join or detach has been called.

就是字面意思,返回 true/false。

// example for thread::joinable
#include <iostream>       // std::cout
#include <thread>         // std::threadvoid mythread() 
{// do stuff...
}int main() 
{std::thread foo;std::thread bar(mythread);std::cout << "Joinable after construction:\n" << std::boolalpha;std::cout << "foo: " << foo.joinable() << '\n';std::cout << "bar: " << bar.joinable() << '\n';if (foo.joinable()) foo.join();if (bar.joinable()) bar.join();std::cout << "Joinable after joining:\n" << std::boolalpha;std::cout << "foo: " << foo.joinable() << '\n';std::cout << "bar: " << bar.joinable() << '\n';return 0;
}

返回结果为:

Joinable after construction:
foo: false
bar: true
Joinable after joining:
foo: false
bar: false

6. 异步

同步:如上面例子,下载完文件,才能和用户交互;
异步:下载文件的过程中,阻塞了,在等待网络请求,这时候将自动切换到和用户交互的线程上,用户体验将不会下降。

  • <future>
    Header with facilities that allow asynchronous access to values set by specific providers, possibly in a different thread.
    Each of these providers (which are either promise or packaged_task objects, or calls to async) share access to a shared state with a future object: the point where the provider makes the shared state ready is synchronized with the point the future object accesses the shared state.

  • std::future:A future is an object that can retrieve a value from some provider object or function, properly synchronizing this access if in different threads.

6.1 异步好帮手:std::async

  • std::async
  • std::future::get

std::async 接受一个带返回值的 lambda,自身返回一个 std::future 对象。lambda 的函数体将在另一个线程里执行。比如下面这个例子:

#include <iostream>
#include <string>
#include <thread>
#include <future>int download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;return 404;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}int main() {std::future<int> fret = std::async([&] {return download("hello.zip"); });interact();int ret = fret.get();std::cout << "Download result: " << ret << std::endl;return 0;
}

因为 download() 返回的是一个 int 类型,所以 std::async 返回的类型是 std::future<int>,future 代表这个 int 现在还没有,但是保证未来会有。
这时可以在 main() 里面做一些别的事情,download() 会持续在后台悄悄运行。最后调用 future 的 get() 方法,如果此时 download() 还没完成,会等待 download() 完成,并获取 download() 的返回值。
在调用 fret.get() 时,才有可能发生阻塞。

6.2 显式地等待:wait()

  • std::future::wait:Wait for ready
    Waits for the shared state to be ready.
    If the shared state is not yet ready (i.e., the provider has not yet set its value or exception), the function blocks the calling thread and waits until it is ready.
    Once the shared state is ready, the function unblocks and returns without reading its value nor throwing its set exception (if any).
    All visible side effects are synchronized between the point the provider makes the shared state ready and the return of this function.

除了 get() 会等待线程执行完毕外,wait() 也可以等待他执行完,但是不会返回其值。(后面应该还是需要使用get()取值)

int main() {std::future<int> fret = std::async([&] {return download("hello.zip"); });interact();std::cout << "Waiting for download complete..." << std::endl;fret.wait();std::cout << "Wait returned!" << std::endl;int ret = fret.get();std::cout << "Download result: " << ret << std::endl;return 0;
}

6.3 等待一段时间:wait_for()

  • std::future::wait_for:Wait for ready during time span
    Waits for the shared state to be ready for up to the time specified by rel_time.
    If the shared state is not yet ready (i.e., the provider has not yet set its value or exception), the function blocks the calling thread and waits until it is ready or until rel_time has elapsed, whichever happens first.
    When the function returns because its shared state is made ready, the value or exception set on the shared state is not read, but all visible side effects are synchronized between the point the provider makes the shared state ready and the return of this function.
    If the shared state contains a deferred function (such as future objects returned by async), the function does not block, returning immediately with a value of future_status::deferred.

wait() 存在一个问题,只要线程没有执行完,wait() 会无限等下去。而 wait_for() 则可以指定一个最长等待时间,用 chrono 里的类表示单位。他会返回一个 std::future_status 表示等待是否成功。如果超过这个时间线程还没有执行完毕,则放弃等待,返回 future_status::timeout;如果线程在指定的时间内执行完毕,则认为等待成功,返回 future_status::ready。

比如下面这个例子中,在等待过程中会有输出:

int main() {std::future<int> fret = std::async([&] {return download("hello.zip"); });interact();while (true) {std::cout << "Waiting for download complete..." << std::endl;auto stat = fret.wait_for(std::chrono::milliseconds(1000));if (stat == std::future_status::ready) {std::cout << "Future is ready!!" << std::endl;break;} else {std::cout << "Future not ready!!" << std::endl;}}int ret = fret.get();std::cout << "Download result: " << ret << std::endl;return 0;
}

在这里插入图片描述
同理还有 wait_until() 其参数是一个时间点。

6.4 另一种用法:std::launch::deferred 做参数

  • std::launch::deferred:Wait for ready during time span
    Deferred: The call to fn is deferred until the shared state of the returned future is accessed (with wait or get). At that point, fn is called and the function is no longer considered deferred. When this call returns, the shared state of the returned future is made ready.

刚刚说 std::async 会创建一个线程在后台执行,如果不想创建一个线程,可以考虑使用 std::launch::deferred 做参数。

std::async 的第一个参数可以设为 std::launch::deferred,这时不会创建一个线程来执行,他只会把 lambda 函数体内的运算推迟到 future 的 get() 被调用时。也就是 main 中的 interact() 计算完毕后。

这种写法,download 的执行仍在主线程中,他只是函数式编程范式意义上的异步,而不涉及到真正的多线程。可以用这个实现惰性求值(lazy evaluation)之类。

int main() {std::future<int> fret = std::async(std::launch::deferred, [&] {return download("hello.zip"); });interact();int ret = fret.get();std::cout << "Download result: " << ret << std::endl;return 0;
}

6.5 std::async 的底层实现:std::promise

  • std::promise:Promise
    A promise is an object that can store a value of type T to be retrieved by a future object (possibly in another thread), offering a synchronization point.
    On construction, promise objects are associated to a new shared state on which they can store either a value of type T or an exception derived from std::exception.
    This shared state can be associated to a future object by calling member get_future. After the call, both objects share the same shared state:
    • The promise object is the asynchronous provider and is expected to set a value for the shared state at some point.
    • The future object is an asynchronous return object that can retrieve the value of the shared state, waiting for it to be ready, if necessary.
      The lifetime of the shared state lasts at least until the last object with which it is associated releases it or is destroyed. Therefore it can survive the promise object that obtained it in the first place if associated also to a future.
int main() {std::promise<int> pret;std::thread t1([&] {auto ret = download("hello.zip");pret.set_value(ret); });std::future<int> fret = pret.get_future();interact();int ret = fret.get();std::cout << "Download result: " << ret << std::endl;t1.join();return 0;
}

如果不想让 std::async 帮你自动创建线程,想要手动创建线程,可以直接用 std::promise。然后在线程返回的时候,用 set_value() 设置返回值。在主线程里,用 get_future() 获取其 std::future 对象,进一步 get() 可以等待并获取线程返回值。这个原理跟 std::async 一样,不过 std::async 帮你包装好了。

6.6 std::future 小贴士

future 为了三五法则,删除了拷贝构造函数。如果需要浅拷贝,实现共享同一个 future 对象,可以用 std::shared_future。
如果不需要返回值,std::async 里 lambda 的返回类型可以为 void, 这时 future 对象的类型为 std::future。
同理有 std::promise,他的 set_value() 不接受参数,仅仅作为同步用,不传递任何实际的值。

#include <iostream>
#include <string>
#include <thread>
#include <future>void download(std::string file) {for (int i = 0; i < 10; i++) {std::cout << "Downloading " << file<< " (" << i * 10 << "%)..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(400));}std::cout << "Download complete: " << file << std::endl;
}void interact() {std::string name;std::cin >> name;std::cout << "Hi, " << name << std::endl;
}int main() {std::shared_future<void> fret = std::async([&] {download("hello.zip"); });auto fret2 = fret;auto fret3 = fret;interact();fret3.wait();std::cout << "Download completed" << std::endl;return 0;
}

7. 互斥量

  • <mutex>
    Header with facilities that allow mutual exclusion (mutex) of concurrent execution of critical sections of code, allowing to explicitly avoid data races.

多线程有个经典案例如下:

int main() {std::vector<int> arr;std::thread t1([&] {for (int i = 0; i < 1000; i++) {arr.push_back(1);}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {arr.push_back(1);}});t1.join();t2.join();return 0;
}

有两个线程试图往同一个数据里堆数据。运行程序的时候会发生崩溃。为什么?vector 不是多线程安全(MT-safe)的容器。多个线程同时访问同一个 vector 会出现数据竞争(data-race)现象。

在这里插入图片描述

7.1 std::mutex:上锁,防止多个线程同时进入某一代码段- std::promise:Promise

  • std::mutex:Mutex class
    A mutex is a lockable object that is designed to signal when critical sections of code need exclusive access, preventing other threads with the same protection from executing concurrently and access the same memory locations.
    mutex objects provide exclusive ownership and do not support recursivity (i.e., a thread shall not lock a mutex it already owns) – see recursive_mutex for an alternative class that does.
    It is guaranteed to be a standard-layout class.

调用 std::mutexlock() 时,会检测 mutex 是否已经上锁。如果没有锁定,则对 mutex 进行上锁。如果已经锁定则陷入等待,直到 mutex 被另一个线程解锁后,才再次上锁(这里很重要的一点是,它是会等待的)。而调用 unlock() 则会进行解锁操作。这样,就可以保证 mtx.lock() 和 mtx.unlock() 之间的代码段,同一时间只有一个线程在执行,从而避免数据竞争。(mutex 是个厕所,A 同学在用了,B 同学就不能进去,要等 A 同学用完了才能进去…)

#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <mutex>int main() {std::vector<int> arr;std::mutex mtx;std::thread t1([&] {for (int i = 0; i < 1000; i++) {mtx.lock();arr.push_back(1);mtx.unlock();}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {mtx.lock();arr.push_back(2);mtx.unlock();}});t1.join();t2.join();return 0;
}

7.2 std::lock_guard:符合 RAII 思想的上锁和解锁

  • std::lock_guard:Lock guard
    A lock guard is an object that manages a mutex object by keeping it always locked.
    On construction, the mutex object is locked by the calling thread, and on destruction, the mutex is unlocked. It is the simplest lock, and is specially useful as an object with automatic duration that lasts until the end of its context. In this way, it guarantees the mutex object is properly unlocked in case an exception is thrown.
    Note though that the lock_guard object does not manage the lifetime of the mutex object in any way: the duration of the mutex object shall extend at least until the destruction of the lock_guard that locks it.

当然,在上面的情况下,要是忘记 unlock() 了,那么不就卡死在这了。

根据 RAII 思想,可将锁的持有视为资源,上锁视为锁的获取,解锁视为锁的释放。std::lock_guard 就是这样一个工具类,他的构造函数里会调用 mtx.lock(),析构函数会调用 mtx.unlock()。从而退出函数作用域时能够自动解锁,避免程序员粗心不小心忘记解锁。(好想法!)

int main() {std::vector<int> arr;std::mutex mtx;std::thread t1([&] {for (int i = 0; i < 1000; i++) {std::lock_guard grd(mtx);arr.push_back(1);}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {std::lock_guard grd(mtx);arr.push_back(2);}});t1.join();t2.join();return 0;
}

7.3 std::unique_lock:也符合 RAII 思想,但自由度更高

  • std::unique_lock:Unique lock
    A unique lock is an object that manages a mutex object with unique ownership in both states: locked and unlocked.
    On construction (or by move-assigning to it), the object acquires a mutex object, for whose locking and unlocking operations becomes responsible.
    The object supports both states: locked and unlocked.
    This class guarantees an unlocked status on destruction (even if not called explicitly). Therefore it is especially useful as an object with automatic duration, as it guarantees the mutex object is properly unlocked in case an exception is thrown.
    Note though, that the unique_lock object does not manage the lifetime of the mutex object in any way: the duration of the mutex object shall extend at least until the destruction of the unique_lock that manages it.

lock_guard 还是存在一定的局限性,比如说必须在作用域结束才会释放,而不能提前释放。也就是说,std::lock_guard 严格在析构时 unlock(),但是有时候我们会希望提前 unlock()。这时可以用 std::unique_lock,它*额外存储了一个 flag 表示是否已经被释放**,会在解构检测这个 flag,如果没有释放,则调用 unlock(),否则不调用。然后可以直接调用 unique_lock 的 unlock() 函数来提前解锁,但是即使忘记解锁也没关系,退出作用域时候他还会自动检查一遍要不要解锁。

int main() {std::vector<int> arr;std::mutex mtx;std::thread t1([&] {for (int i = 0; i < 1000; i++) {std::unique_lock grd(mtx);arr.push_back(1);}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {std::unique_lock grd(mtx);arr.push_back(2);grd.unlock();printf("outside of lock\n");// grd.lock();  // 如果需要,还可以重新上锁}});t1.join();t2.join();return 0;
}

7.3.1 std::unique_lock:用 std::defer_lock 作为参数

std::unique_lock 的构造函数还可以有一个额外参数,那就是 std::defer_lock。指定了这个参数的话,std::unique_lock 不会在构造函数中调用 mtx.lock(),需要之后再手动调用 grd.lock() 才能上锁。好处依然是即使忘记 grd.unlock() 也能够自动调用 mtx.unlock()

int main() {std::vector<int> arr;std::mutex mtx;std::thread t1([&] {for (int i = 0; i < 1000; i++) {std::unique_lock grd(mtx);arr.push_back(1);}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {std::unique_lock grd(mtx, std::defer_lock);printf("before the lock\n");grd.lock();arr.push_back(2);grd.unlock();printf("outside of lock\n");}});t1.join();t2.join();return 0;
}

例程中开始 grd 是没有上锁的,在调用 grd.lock() 后,才上的锁。

可以看一下 std::defer_lock_t,是个空的类,其实就是为了做构造函数的重载。

7.4 多个对象?每个对象一个 mutex 即可

mtx1 用来锁定 arr1,mtx2 用来锁定 arr2。不同的对象,各有一个 mutex,独立地上锁,可以避免不必要的锁定,提升高并发时的性能。(即两个厕所两把锁…)

还用了一个 {} 包住 std::lock_guard,限制其变量的作用域,从而可以让他在 } 之前解构并调用 unlock(),即在确定的时间解锁,也避免了和下面一个 lock_guard 变量名冲突。

int main() {std::vector<int> arr1;std::mutex mtx1;std::vector<int> arr2;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {{std::lock_guard grd(mtx1);arr1.push_back(1);}{std::lock_guard grd(mtx2);arr2.push_back(1);}}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {{std::lock_guard grd(mtx1);arr1.push_back(2);}{std::lock_guard grd(mtx2);arr2.push_back(2);}}});t1.join();t2.join();return 0;
}

7.5 如果上锁失败,不要等待:try_lock()

  • std::try_lock:Try to lock multiple mutexes
    Attempts to lock all the objects passed as arguments using their try_lock member functions (non-blocking).
    The function calls the try_lock member function for each argument (first a, then b, and eventually the others in cde, in the same order), until either all calls are successful, or as soon as one of the calls fails (either by returning false or throwing an exception).
    If the function ends because a call fails, unlock is called on all objects for which the call to try_lock was successful, and the function returns the argument order number of the object whose lock failed. No further calls are performed for the remaining objects in the argument list.

我们说过 lock() 如果发现 mutex 已经上锁的话,会等待他直到他解锁。也可以用无阻塞的 try_lock(),他在上锁失败时不会陷入等待,而是直接返回 false;如果上锁成功,则会返回 true。比如下面这个例子,第一次上锁,因为还没有人上锁,所以成功了,返回 true。第二次上锁,由于自己已经上锁,所以失败了,返回 false。

#include <cstdio>
#include <mutex>std::mutex mtx1;int main() {if (mtx1.try_lock())printf("succeed\n");elseprintf("failed\n");if (mtx1.try_lock())printf("succeed\n");elseprintf("failed\n");mtx1.unlock();return 0;
}

7.6 只等待一段时间:try_lock_for()

try_lock() 碰到已经上锁的情况,会立即返回 false。如果需要等待,但仅限一段时间,可以用 std::timed_mutex 的 try_lock_for() 函数,他的参数是最长等待时间,同样是由 chrono 指定时间单位。超过这个时间还没成功就会“不耐烦地”失败并返回 false;如果这个时间内上锁成功则返回 true。同理还有接受时间点的 try_lock_until()

std::timed_mutex mtx1;int main() {if (mtx1.try_lock_for(std::chrono::milliseconds(500)))printf("succeed\n");elseprintf("failed\n");if (mtx1.try_lock_for(std::chrono::milliseconds(500)))printf("succeed\n");elseprintf("failed\n");mtx1.unlock();return 0;
}

7.7 std::unique_lock补充

7.7.1 用 std::try_to_lock 做参数

和无参数相比,他会调用 mtx1.try_lock() 而不是 mtx1.lock()。之后,可以用 grd.owns_lock() 判断是否上锁成功。

#include <cstdio>
#include <thread>
#include <mutex>int main() {std::mutex mtx;std::thread t1([&] {std::unique_lock grd(mtx, std::try_to_lock);if (grd.owns_lock())printf("t1 success\n");elseprintf("t1 failed\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});std::thread t2([&] {std::unique_lock grd(mtx, std::try_to_lock);if (grd.owns_lock())printf("t2 success\n");elseprintf("t2 failed\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});t1.join();t2.join();return 0;
}

7.7.2 用 std::adopt_lock 做参数

std::defer_lock 是之后再上锁,而 std::adopt_lock 是之前已经上锁了,再告诉它(如果没上锁,则上锁?)。
如果当前 mutex 已经上锁了,但是之后仍然希望用 RAII 思想在析构时候自动调用 unlock(),可以用 std::adopt_lock 作为 std::unique_lock 或 std::lock_guard 的第二个参数,这时他们会默认 mtx 已经上锁。

int main() {std::mutex mtx;std::thread t1([&] {std::unique_lock grd(mtx);printf("t1 owns the lock\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});std::thread t2([&] {mtx.lock();std::unique_lock grd(mtx, std::adopt_lock);printf("t2 owns the lock\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});t1.join();t2.join();return 0;
}

顺带一提,std::try_to_lockstd::adopt_lock 也都是空的,目的就是做构造函数重载。

7.8 std::unique_lock 和 std::mutex 具有同样的接口

std::unique_lock 本身还可以再调用 std::lock_guard
其实 std::unique_lock 具有 mutex 的所有成员函数:lock(), unlock(), try_lock(), try_lock_for() 等。除了他会在解构时按需自动调用 unlock()。因为 std::lock_guard 无非是调用其构造参数名为 lock() 的成员函数,所以 std::unique_lock 也可以作为 std::lock_guard 的构造参数!这种只要具有某些指定名字的成员函数,就判断一个类是否满足某些功能的思想,在 Python 称为鸭子类型,而 C++ 称为 concept(概念)。比起虚函数和动态多态的接口抽象,concept 使实现和接口更加解耦合且没有性能损失。

#include <cstdio>
#include <thread>
#include <mutex>int main() {std::mutex mtx;std::thread t1([&] {std::unique_lock grd(mtx, std::defer_lock);std::lock_guard grd2(grd);printf("t1 owns the lock\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});std::thread t2([&] {std::unique_lock grd(mtx, std::defer_lock);std::lock_guard grd2(grd);printf("t2 owns the lock\n");std::this_thread::sleep_for(std::chrono::milliseconds(1000));});t1.join();t2.join();return 0;
}

8. 死锁

由于同时执行的两个线程,他们中发生的指令不一定是同步的,因此有可能出现这种情况:

  • t1 执行 mtx1.lock()。
  • t2 执行 mtx2.lock()。
  • t1 执行 mtx2.lock():失败,陷入等待
  • t2 执行 mtx1.lock():失败,陷入等待

双方都在等着对方释放锁,但是因为等待而无法释放锁,从而要无限制等下去。这种现象称为死锁(dead-lock)。(同时锁住多个 mutex)

#include <iostream>
#include <string>
#include <thread>
#include <mutex>int main() {std::mutex mtx1;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {mtx1.lock();mtx2.lock();mtx2.unlock();mtx1.unlock();}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {mtx2.lock();mtx1.lock();mtx1.unlock();mtx2.unlock();}});t1.join();t2.join();return 0;
}

8.1 解决方案

8.1.1 永远不要同时持有两个锁

最为简单的方法,就是一个线程永远不要同时持有两个锁,分别上锁,这样也可以避免死锁。因此这里双方都在 mtx1.unlock() 之后才 mtx2.lock(),从而也不会出现一方等着对方的同时持有了对方等着的锁的情况。

int main() {std::mutex mtx1;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {mtx1.lock();mtx1.unlock();mtx2.lock();mtx2.unlock();}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {mtx2.lock();mtx2.unlock();mtx1.lock();mtx1.unlock();}});t1.join();t2.join();return 0;
}

8.1.2 保证双方上锁顺序一致

其实,只需保证双方上锁的顺序一致,即可避免死锁。因此这里调整 t2 也变为先锁 mtx1,再锁 mtx2。这时,无论实际执行顺序是怎样,都不会出现一方等着对方的同时持有了对方等着的锁的情况。

int main() {std::mutex mtx1;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {mtx1.lock();mtx2.lock();mtx2.unlock();mtx1.unlock();}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {mtx1.lock();mtx2.lock();mtx2.unlock();mtx1.unlock();}});t1.join();t2.join();return 0;
}

8.1.3 用 std::lock 同时对多个上锁

如果没办法保证上锁顺序一致,可以用标准库的 std::lock(mtx1, mtx2, …) 函数,一次性对多个 mutex 上锁。他接受任意多个 mutex 作为参数,并且他保证在无论任意线程中调用的顺序是否相同,都不会产生死锁问题

int main() {std::mutex mtx1;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {std::lock(mtx1, mtx2);mtx1.unlock();mtx2.unlock();}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {std::lock(mtx2, mtx1);mtx2.unlock();mtx1.unlock();}});t1.join();t2.join();return 0;
}

8.1.4 std::lock 的 RAII 版本:std::scoped_lock

和 std::lock_guard 相对应,std::lock 也有 RAII 的版本 std::scoped_lock。只不过他可以同时对多个 mutex 上锁。

int main() {std::mutex mtx1;std::mutex mtx2;std::thread t1([&] {for (int i = 0; i < 1000; i++) {std::scoped_lock grd(mtx1, mtx2);// do something}});std::thread t2([&] {for (int i = 0; i < 1000; i++) {std::scoped_lock grd(mtx2, mtx1);// do something}});t1.join();t2.join();return 0;
}

8.2 同一个线程重复调用 lock() 也会造成死锁

除了两个线程同时持有两个锁会造成死锁外,即使只有一个线程一个锁,如果 lock() 以后又调用 lock(),也会造成死锁。比如下面示例的 func 函数,上了锁之后,又调用了 other 函数,他也需要上锁。而 other 看到 mtx1 已经上锁,还以为是别的线程上的锁,于是陷入等待。殊不知是调用他的 func 上的锁,other 陷入等待后 func 里的 unlock() 永远得不到调用。

#include <iostream>
#include <mutex>std::mutex mtx1;void other() {mtx1.lock();// do somethingmtx1.unlock();
}void func() {mtx1.lock();other();mtx1.unlock();
}int main() {func();return 0;
}

8.2.1 解决1:other 里不要再上锁

遇到这种情况最好是把 other 里的 lock() 去掉,并在其文档中说明:“other 不是线程安全的,调用本函数之前需要保证某 mutex 已经上锁。”(就是写个文档提醒自己小心)

std::mutex mtx1;/// NOTE: please lock mtx1 before calling other()
void other() {// do something
}void func() {mtx1.lock();other();mtx1.unlock();
}int main() {func();return 0;
}

8.2.2 改用 std::recursive_mutex

如果实在不能改的话,可以用 std::recursive_mutex。他会自动判断是不是同一个线程 lock() 了多次同一个锁,如果是则让计数器加1,之后 unlock() 会让计数器减1,减到0时才真正解锁。但是相比普通的 std::mutex 有一定性能损失。同理还有 std::recursive_timed_mutex,如果你同时需要 try_lock_for() 的话。

std::recursive_mutex mtx1;void other() {mtx1.lock();// do somethingmtx1.unlock();
}void func() {mtx1.lock();other();mtx1.unlock();
}int main() {func();return 0;
}

9. 数据结构

9.1 封装一个线程安全的 vector

在前面章节说过,vector 不是多线程安全的容器。多个线程同时访问同一个 vector 会出现数据竞争(data-race)现象。

因此,可以用一个类封装一下对 vector 的访问,使其访问都受到一个 mutex 的保护。

class MTVector {std::vector<int> m_arr;std::mutex m_mtx;public:void push_back(int val) {m_mtx.lock();m_arr.push_back(val);m_mtx.unlock();}size_t size() const {m_mtx.lock();size_t ret = m_arr.size();m_mtx.unlock();return ret;}
};int main() {MTVector arr;std::thread t1([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(i);}});std::thread t2([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(1000 + i);}});t1.join();t2.join();std::cout << arr.size() << std::endl;return 0;
}

然而却出错了:因为 size() 是 const 函数,而 mutex::lock() 却不是 const 的。(这里将size()后面的const去掉就可以编译了,但写成size_t size() const{}肯定正规多了,毕竟原来的vector就是这样定义的)
在这里插入图片描述

9.1.1 逻辑上 const 而部分成员非 const:mutable

我们要为了支持 mutex 而放弃声明 size() 为 const 吗?不必,size() 在逻辑上仍是 const 的。因此,为了让 this 为 const 时仅仅给 m_mtx 开后门,可以用 mutable 关键字修饰它,从而所有成员里只有他不是 const 的。(就是多加了一个mutable关键字)

class MTVector {std::vector<int> m_arr;mutable std::mutex m_mtx;public:void push_back(int val) {m_mtx.lock();m_arr.push_back(val);m_mtx.unlock();}size_t size() const {m_mtx.lock();size_t ret = m_arr.size();m_mtx.unlock();return ret;}
};

9.2 为什么需要读写锁?

之前说过 mutex 就像厕所,同一时刻只有一个人能上。但是如果“上”有两种方式呢?假设在平行世界,厕所不一定是用来拉的,还可能是用来喝的,喝厕所里的水时,可以多个人插着吸管一起喝。而拉的时候,只能一个人独占厕所,不能多个人一起往里面拉。
喝水的人如果发现厕所里已经有人在拉,那他也不能去喝,否则会喝到“脏数据”。

  • 结论:可以共享必须独占,且写和读不能共存
    针对这种更具体的情况,又发明了读写锁,它允许的状态有:
    1. n个人读取,没有人写入。
    2. 1个人写入,没有人读取。
    3. 没有人读取,也没有人写入。

9.2.1 读写锁:shared_mutex

为此,标准库提供了 std::shared_mutex
上锁时,要指定你的需求是拉还是喝,负责调度的读写锁会帮你判断要不要等待
这里 push_back() 需要修改数据,因此需求为,使用 lock()unlock() 的组合。
而 size() 则只要读取数据,不修改数据,因此可以和别人共享一起,使用 lock_shared()unlock_shared() 的组合。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <shared_mutex>class MTVector {std::vector<int> m_arr;mutable std::shared_mutex m_mtx;public:void push_back(int val) {std::unique_lock grd(m_mtx);m_arr.push_back(val);}size_t size() const {std::shared_lock grd(m_mtx);return m_arr.size();}
};int main() {MTVector arr;std::thread t1([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(i);}});std::thread t2([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(1000 + i);}});t1.join();t2.join();std::cout << arr.size() << std::endl;return 0;
}

9.2.2 std::shared_lock:符合 RAII 思想的 lock_shared()

正如 std::unique_lock 针对 lock(),也可以用 std::shared_lock 针对 lock_shared()。这样就可以在函数体退出时自动调用 unlock_shared(),更加安全了。
shared_lock 同样支持 defer_lock 做参数,owns_lock() 判断等。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <shared_mutex>class MTVector {std::vector<int> m_arr;mutable std::shared_mutex m_mtx;public:void push_back(int val) {std::unique_lock grd(m_mtx);m_arr.push_back(val);}size_t size() const {std::shared_lock grd(m_mtx);return m_arr.size();}
};int main() {MTVector arr;std::thread t1([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(i);}});std::thread t2([&] () {for (int i = 0; i < 1000; i++) {arr.push_back(1000 + i);}});t1.join();t2.join();std::cout << arr.size() << std::endl;return 0;
}

9.2.3 只需一次性上锁,且符合 RAII 思想:访问者模式

再来看下面一个例子,MTVector为用来存储数据的类,而Accessor是用来访问数据的类,它们被区分了开来。这里的一个原因就是使用锁std::unique_lock<std::mutex> m_guard;。比如前面一小节中,我们使用std::unique_lock grd(m_mtx);std::unique_lock grd(m_mtx);在每一次循环中上锁和解锁,这样非常低效,而用访问者就可以进行一次性上锁。

如何做到一次性上锁呢?因为访问者里面有一个unique,_lock类型,就是访问者初始化的时候,会先锁住这个mutex。然后在访问者axr析构的时候,才会去把这个锁解锁掉。也就是说这里面只定义了一次上锁一次解锁,从而它就合并了多次上锁(这里有个push_back的loop调用,不需要每一个push_back上个锁再解锁),对性能有帮助,且能够分离实际对象的存储与访问-------存储是外面一个类,而访问是里面额外的一个类。可以通过access来获取一个存储对象的访问者类。(但是这里的示例没有反应读共享的感觉)

特别是在 GPU 上,这个访问者模式就很重要,因为没办法把一个 vector 拷到 GPU 上。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>class MTVector {std::vector<int> m_arr;std::mutex m_mtx;public:class Accessor {MTVector &m_that;std::unique_lock<std::mutex> m_guard;public:Accessor(MTVector &that): m_that(that), m_guard(that.m_mtx){}void push_back(int val) const {return m_that.m_arr.push_back(val);}size_t size() const {return m_that.m_arr.size();}};Accessor access() {return {*this};}
};int main() {MTVector arr;std::thread t1([&] () {auto axr = arr.access();for (int i = 0; i < 1000; i++) {axr.push_back(i);}});std::thread t2([&] () {auto axr = arr.access();for (int i = 0; i < 1000; i++) {axr.push_back(1000 + i);}});t1.join();t2.join();std::cout << arr.access().size() << std::endl;return 0;
}

Accessor 或者说 Viewer 模式,常用于设计 GPU 容器 OpenVDB 数据结构的访问,也可以采用 Accessor 的设计……并且还有 ConstAccessor 和 Accessor 两种,分别对应于读和写。

10. 条件变量

前面说的互斥量是防止多个线程,同时访问一个数据;而条件变量,更像是一种信号量之类的东西-----只有某个事件发生了之后,这个线程才能继续执行。

10.1 条件变量:等待被唤醒

  • std::condition_variable:A condition variable is an object able to block the calling thread until notified to resume.
    It uses a unique_lock (over a mutex) to lock the thread when one of its wait functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the same condition_variable object.
  • std::condition_variable::wait:Wait until notified
    The execution of the current thread (which shall have locked lck’s mutex) is blocked until notified.
    At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.
    Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).
    Generally, the function is notified to wake up by a call in another thread either to member notify_one or to member notify_all. But certain implementations may produce spurious wake-up calls without any of these functions being called. Therefore, users of this function shall ensure their condition for resumption is met.

cv.wait(lck) 将会让当前线程陷入等待
其他线程中调用 cv.notify_one() 则会唤醒那个陷入等待的线程
可以发现 std::condition_variable 必须和 std::unique_lock<std::mutex> 一起用,稍后会解释原因。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>int main() {std::condition_variable cv;std::mutex mtx;std::thread t1([&] {std::unique_lock lck(mtx);cv.wait(lck);std::cout << "t1 is awake" << std::endl;});std::this_thread::sleep_for(std::chrono::milliseconds(400));std::cout << "notifying..." << std::endl;cv.notify_one();  // will awake t1t1.join();return 0;
}

输出为:
在这里插入图片描述

10.2 条件变量:等待某一条件成真

还可以额外指定一个参数,变成 cv.wait(lck, expr) 的形式,其中 expr 是个 lambda 表达式,只有其返回值为 true 时才会真正唤醒,否则继续等待。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>int main() {std::condition_variable cv;std::mutex mtx;bool ready = false;std::thread t1([&] {std::unique_lock lck(mtx);cv.wait(lck, [&] { return ready; });std::cout << "t1 is awake" << std::endl;});std::cout << "notifying not ready" << std::endl;cv.notify_one();  // useless now, since ready = falseready = true;std::cout << "notifying ready" << std::endl;cv.notify_one();  // awakening t1, since ready = truet1.join();return 0;
}

输出为:
在这里插入图片描述

10.3 条件变量:多个等待者

cv.notify_one() 只会唤醒其中一个等待中的线程(经过测试感觉像是随机的一个),而 cv.notify_all() 会唤醒全部。这就是为什么 wait() 需要一个 unique_lock 作为参数,因为要保证多个线程被唤醒时,只有一个能够被启动。如果不需要,在 wait() 返回后调用 lck.unlock() 即可。顺便一提,wait() 的过程中会暂时 unlock() 这个锁。

int main() {std::condition_variable cv;std::mutex mtx;std::thread t1([&] {std::unique_lock lck(mtx);cv.wait(lck);std::cout << "t1 is awake" << std::endl;});std::thread t2([&] {std::unique_lock lck(mtx);cv.wait(lck);std::cout << "t2 is awake" << std::endl;});std::thread t3([&] {std::unique_lock lck(mtx);cv.wait(lck);std::cout << "t3 is awake" << std::endl;});std::this_thread::sleep_for(std::chrono::milliseconds(400));std::cout << "notifying one" << std::endl;cv.notify_one();  // awakening t1 onlystd::this_thread::sleep_for(std::chrono::milliseconds(400));std::cout << "notifying all" << std::endl;cv.notify_all();  // awakening t1 and t2t1.join();t2.join();t3.join();return 0;
}

输出结果为:
在这里插入图片描述

10.3.1 案例:实现生产者-消费者模式

下面这个例子类似于消息队列:
生产者:厨师,往 foods 队列里推送食品,推送后会通知消费者来用餐。
消费者:等待 foods 队列里有食品,没有食品则陷入等待,直到被通知。

int main() {std::condition_variable cv;std::mutex mtx;std::vector<int> foods;std::thread t1([&] {for (int i = 0; i < 2; i++) {std::unique_lock lck(mtx);cv.wait(lck, [&] {return foods.size() != 0;});auto food = foods.back();foods.pop_back();lck.unlock();std::cout << "t1 got food:" << food << std::endl;}});std::thread t2([&] {for (int i = 0; i < 2; i++) {std::unique_lock lck(mtx);cv.wait(lck, [&] {return foods.size() != 0;});auto food = foods.back();foods.pop_back();lck.unlock();std::cout << "t2 got food:" << food << std::endl;}});foods.push_back(42);foods.push_back(233);cv.notify_one();foods.push_back(666);foods.push_back(4399);cv.notify_all();t1.join();t2.join();return 0;
}

结果为(感觉这例子不太对劲,这样菜会上的乱七八糟的。且输出因为线程顺序,每次打印的不太一样):
在这里插入图片描述

10.3.2 条件变量:将 foods 队列封装成类

template <class T>
class MTQueue {std::condition_variable m_cv;std::mutex m_mtx;std::vector<T> m_arr;public:T pop() {std::unique_lock lck(m_mtx);m_cv.wait(lck, [this] { return !m_arr.empty(); });T ret = std::move(m_arr.back());m_arr.pop_back();return ret;}auto pop_hold() {std::unique_lock lck(m_mtx);m_cv.wait(lck, [this] { return !m_arr.empty(); });T ret = std::move(m_arr.back());m_arr.pop_back();return std::pair(std::move(ret), std::move(lck));}void push(T val) {std::unique_lock lck(m_mtx);m_arr.push_back(std::move(val));m_cv.notify_one();}void push_many(std::initializer_list<T> vals) {std::unique_lock lck(m_mtx);std::copy(std::move_iterator(vals.begin()),std::move_iterator(vals.end()),std::back_insert_iterator(m_arr));m_cv.notify_all();}
};int main() {MTQueue<int> foods;std::thread t1([&] {for (int i = 0; i < 2; i++) {auto food = foods.pop();std::cout << "t1 got food:" << food << std::endl;}});std::thread t2([&] {for (int i = 0; i < 2; i++) {auto food = foods.pop();std::cout << "t2 got food:" << food << std::endl;}});foods.push(42);foods.push(233);foods.push_many({666, 4399});t1.join();t2.join();return 0;
}

输出结果为:
在这里插入图片描述

10.4 注意事项

  1. std::condition_variable 仅仅支持 std::unique_lockstd::mutex 作为 wait 的参数,如果需要用其他类型的 mutex 锁,可以用 std::condition_variable_any。
  2. 他还有 wait_for() 和 wait_until() 函数,分别接受 chrono 时间段和时间点作为参数。详见:https://en.cppreference.com/w/cpp/thread/condition_variable/wait_for。

11. 原子操作

前面都是接近 里的对象,当然我们也可以直接从硬件层面上去操作,这样子更高效。而且操作系统提供的 mutex,也是基于硬件层面实现的。

11.1 经典案例:多个线程修改同一个计数器

来看下面这个示例:

int main() {int counter = 0;std::thread t1([&] {for (int i = 0; i < 10000; i++) {counter += 1;}});std::thread t2([&] {for (int i = 0; i < 10000; i++) {counter += 1;}});t1.join();t2.join();std::cout << counter << std::endl;return 0;
}

counter += 1 看着像是一条独立指令,但多个线程同时往一个 int 变量里累加,这样肯定会出错,因为 counter += i 在 CPU 看来会变成三个指令:

  1. 读取 counter 变量到 rax 寄存器
  2. rax 寄存器的值加上 1
  3. 把 rax 写入到 counter 变量

即使编译器优化成 add [counter], 1 也没用,因为现代 CPU 为了高效,使用了大量奇技淫巧,比如他会把一条汇编指令拆分成很多微指令 (micro-ops),三个甚至有点保守估计了。

问题是,如果有多个线程同时运行,顺序是不确定的:

  1. t1:读取 counter 变量,到 rax 寄存器
  2. t2:读取 counter 变量,到 rax 寄存器
  3. t1:rax 寄存器的值加上 1
  4. t2:rax 寄存器的值加上 1
  5. t1:把 rax 写入到 counter 变量
  6. t2:把 rax 写入到 counter 变量

如果是这种顺序,最后 t1 的写入就被 t2 覆盖了,从而 counter 只增加了 1,而没有像预期的那样增加 2。更不用说现代 CPU 还有高速缓存,乱序执行,指令级并行等优化策略,你根本不知道每条指令实际的先后顺序。

11.2 暴力解决:用 mutex 上锁

所以说,最暴力的解决办法就是使用 mutex 上锁。这样的确可以防止多个线程同时修改 counter 变量,从而不会冲突。

std::thread t1([&] {for (int i = 0; i < 10000; i++) {mtx.lock();counter += 1;mtx.unlock();}
});std::thread t2([&] {for (int i = 0; i < 10000; i++) {mtx.lock();counter += 1;mtx.unlock();}
});

问题:mutex 太过重量级,他会让线程被挂起,从而需要通过系统调用,进入内核层,调度到其他线程执行,有很大的开销。(mutex是操作系统来维护的,要使用操作系统上锁,需要先进入到内核态,再进入到用户态,甚至要切换到另一个线程)

11.3 建议用 atomic:有专门的硬件指令加持

这个时候就引出了更轻量级的 atomic,对它的 += 等操作,会被编译器转换成专门的指令。

CPU 识别到该指令时,会锁住内存总线,放弃乱序执行等优化策略(将该指令视为一个同步点,强制同步掉之前所有的内存操作),从而向你保证该操作是原子 (atomic) 的(取其不可分割之意),不会加法加到一半另一个线程插一脚进来。

对于程序员,只需把 int 改成 atomic 即可,也不必像 mutex 那样需要手动上锁解锁,因此用起来也更直观。

#include <iostream>
#include <thread>
#include <atomic>int main() {std::atomic<int> counter = 0;std::thread t1([&] {for (int i = 0; i < 10000; i++) {counter += 1;}});std::thread t2([&] {for (int i = 0; i < 10000; i++) {counter += 1;}});t1.join();t2.join();std::cout << counter << std::endl;return 0;
}

11.3.1 注意:请用 +=,不要让 + 和 = 分开

不过要注意了,这种写法:

  1. counter = counter + 1; // 错,不能保证原子性
  2. counter += 1; // OK,能保证原子性
  3. counter++; // OK,能保证原子性

比如说使用如下加法,得到的最终结果是不等于20000的:

std::thread t1([&] {for (int i = 0; i < 10000; i++) {counter = counter + 1;}
});std::thread t2([&] {for (int i = 0; i < 10000; i++) {counter = counter + 1;}
});

11.3.2 调用函数名

除了用方便的运算符重载之外,还可以直接调用相应的函数名,比如:

  • fetch_add 对应于 +=
  • store 对应于 =
  • load 用于读取其中的 int 值
int main() {std::atomic<int> counter;counter.store(0);std::thread t1([&] {for (int i = 0; i < 10000; i++) {counter.fetch_add(1);}});std::thread t2([&] {for (int i = 0; i < 10000; i++) {counter.fetch_add(1);}});t1.join();t2.join();std::cout << counter.load() << std::endl;return 0;
}

11.3.3 fetch_add:会返回其旧值

int old = atm.fetch_add(val)

除了会导致 atm 的值增加 val 外,还会返回 atm 增加前的值,存储到 old。这个特点使得他可以用于并行地往一个列表里追加数据:追加写入的索引就是 fetch_add 返回的旧值。
当然这里也可以 counter++,不过要追加多个的话还是得用到 counter.fetch_add(n)。

int main() {std::atomic<int> counter;counter.store(0);std::vector<int> data(20000);std::thread t1([&] {for (int i = 0; i < 10000; i++) {int index = counter.fetch_add(1);data[index] = i;}});std::thread t2([&] {for (int i = 0; i < 10000; i++) {int index = counter.fetch_add(1);data[index] = i + 10000;}});t1.join();t2.join();std::cout << data[10000] << std::endl;return 0;
}

11.3.4 exchange:读取的同时写入

exchange(val) 会把 val 写入原子变量,同时返回其旧的值。

int main() {std::atomic<int> counter;counter.store(0);int old = counter.exchange(3);std::cout << "old=" << old << std::endl;  // 0int now = counter.load();std::cout << "cnt=" << now << std::endl;  // 3return 0;
}

11.3.5 compare_exchange_strong:读取,比较是否相等,相等则写入

compare_exchange_strong(old, val) 会读取原子变量的值,比较它是否和 old 相等:

  • 如果不相等,则把原子变量的值写入 old。
  • 如果相等,则把 val 写入原子变量(在下面例子中为counter)。
  • 返回一个 bool 值,表示是否相等。

注意 old 这里传的其实是一个引用,因此 compare_exchange_strong 可修改他的值。

int main() {boolalpha(std::cout);std::atomic<int> counter;counter.store(2);int old = 1;bool equal = counter.compare_exchange_strong(old, 3);std::cout << "equal=" << equal << std::endl;  // falsestd::cout << "old=" << old << std::endl;  // 2int now = counter.load();std::cout << "cnt=" << now << std::endl;  // 3old = 2;equal = counter.compare_exchange_strong(old, 3);std::cout << "equal=" << equal << std::endl;  // truestd::cout << "old=" << old << std::endl;  // 1now = counter.load();std::cout << "cnt=" << now << std::endl;  // 3return 0;
}

11.3.6 方便理解的伪代码

为了方便理解,可以假想 atomic 里面是这样实现的:

template <class T>
struct atomic {std::mutex mtx;int cnt;int store(int val) {std::lock_guard _(mtx);cnt = val;}int load() const {std::lock_guard _(mtx);return cnt;}int fetch_add(int val) {std::lock_guard _(mtx);int old = cnt;cnt += val;return old;}int exchange(int val) {std::lock_guard _(mtx);int old = cnt;cnt = val;return old;}bool compare_exchange_strong(int &old, int val) {std::lock_guard _(mtx);if (cnt == old) {cnt = val;return true;} else {old = cnt;return false;}}
};

可以看到其中 compare_exchange_strong 的逻辑最为复杂,一般简称 CAS (compare-and-swap),他是并行编程最常用的原子操作之一。实际上任何 atomic 操作,包括 fetch_add,都可以基于 CAS 来实现:这就是 Taichi 实现浮点数 atomic_add 的方法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_410162.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL学习十九、使用游标

游标&#xff08;cursor&#xff09;是一个存储在 DBMS 服务器上的数据库查询&#xff0c; 它不是一条 SELECT 语句&#xff0c;而是被该语句检索出来的结果集。在存储了 游标之后&#xff0c;应用程序可以根据需要滚动或浏览其中的数据。 我们通常的检索操作会返回一组称为结…

vue3+antd中使用Dayjs实现输出的日期格式化,和限制自定义日期选择器的可选范围

场景复现 在vue3antd项目中用到了日期选择器&#xff0c;但是默认的日期选择的结果是标准的日期格式&#xff0c;我们往往需要对最后的结果进行一定的格式化输出 一般输出的是这种标准的数据格式 如果我们想对时间进行指定的格式化输出&#xff0c;通常大家会想到moment&…

如何在页面中制作悬浮发布按钮弹窗

效果展示&#xff1a; 前置准备&#xff1a; 1.已搭建好&#xff0c;待添加悬浮层的页面 2.icon素材 具体步骤&#xff1a;&#xff08;3&#xff09; 1.添加悬浮层页面 2.配置悬浮层关闭触发器 3.配置首页发布icon触发器和动画 步骤分解&#xff1a; 1.添加悬浮层页面 1.1…

2022 年跨境电商要尝试的 25 个黑五营销技巧

关键词&#xff1a;黑五营销、黑色星期五活动、跨境电商黑五 我们汇总了以下最佳跨境电商黑五创意清单&#xff1a; 黑五营销技巧分享 如何宣传您的黑色星期五优惠 小型企业的黑五营销创意 黑五营销提示 随意跳到您最感兴趣的部分&#xff0c;或通读它们&#xff0c;看看…

JAVA序列化和反序列化学习笔记

0x01 开始学习JAVA反序列化&#xff0c;参考 《安全漫谈》和feng师傅的文章一步一步来&#xff0c;希望 能赶在这个学期学完java最基础的东西&#xff0c; 笔记做到这里方便自己查阅&#xff0c;也是事先实操了一下 &#xff0c;才写的笔记 概念&#xff1a; JAVA 序列化 就是…

program arguments,vm arguments,environment variable

作者:david_zhang@sh 【转载时请以超链接形式标明文章】 https://www.cnblogs.com/david-zhang-index/p/16846493.html 参数太多,傻傻分不清楚,简单说 1,program arguments是main函数args[]参数 2,vm arguments是java环境变量 3,environment variable是jvm环境变量 看代码…

华为设备配置NAT原理与示例

网络地址转换NAT 文章目录网络地址转换NAT1 NAT概述1.1 NAT产生的技术背景1.2 私有IP地址1.3 NAT技术原理2 静态NAT2.1 静态NAT原理2.2 静态NAT转换示例2.3 静态NAT配置介绍2.4 静态NAT配置示例3 动态NAT3.1 动态NAT原理3.2 动态NAT转换示例3.3 动态NAT配置介绍3.4 动态NAT配置…

resultMap结果映射

文章目录一、resulrMap结果映射二、驼峰命名自动映射查询结果的列名和Java对象的属性名对应不上怎么办&#xff1f; *第一种方式&#xff1a;as给列名起别名 *第二种方式&#xff1a;使用resultMap进行结果映射 *第三种方式&#xff1a;是否开启驼峰命名自动映射&#xff08;配…

算法学习:动态规划

14天阅读挑战赛 努力是为了不平庸~ 系列文章目录 第一章 算法简介 第二章 贪心算法 第三章 分治法 第四章 动态规划 目录系列文章目录2.0兔子序列2.1动态规划基础2.2最长的公共子序列2.2.1问题描述&#xff1a;2.2.2分析问题&设计思路&#xff1a;2.2.3图解思路&#xff1…

Python抓取我的CSDN粉丝数,白嫖GithubAction自动抓取

《Python抓取我的CSDN粉丝数&#xff0c;白嫖GithubAction自动抓取》 一.介绍 这段时间我想申请CSDN的博客专家认证&#xff0c;但是我发现我的总访问量不够&#xff08;博客专家的总访问量要大于20万&#xff09;&#xff0c;所以我就想把我的CSDN每天的 【总访问量】&#…

芯片与自动驾驶技术漫谈

芯片与自动驾驶技术漫谈 从芯片到系统国产,信创产业如何4步走上自主路? 信创产业发展是国家经济数字化转型、提升产业链发展的关键。我国明确了“数字中国”建设战略,抢占数字经济产业链制高点。推进信创产业的发展,促进信创产业在区域性落地生根,带动传统IT信息产业转型,…

【光通信】常见光模块与光纤收发器说明及作用区别

&#xff1a;单纤收发器是指采用的是单模光缆 单纤收发器是只用一根芯&#xff0c;两端都接这根芯&#xff0c;两端的收发器采用不同的光波长&#xff0c;所以能在一根芯里传输光信号。 双纤收发器就是采用了两根芯&#xff0c;一根发送一根接收&#xff0c;一端是发的另一端就…

MongoDB 分片集群均衡器导致的性能下降

近期&#xff0c;有人反馈其mongodb分片集群&#xff0c;在加载处理大批量数据时&#xff0c;程序处理十分缓慢并且应用还会报错&#xff1a;version mismatch detected for 。现将分析汇总如下备用。 一、问题现象 负责同事反馈9月1日18:52分左右&#xff0c;应用报错version…

计算机毕业设计(附源码)python医院预约挂号管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

学习笔记-php伪协议

伪协议 相关文章 & Source & Reference PHP伪协议的妙用 filter协议 php://filter 是一种元封装器&#xff0c; 设计用于数据流打开时的筛选过滤应用。这对于一体式(all-in-one)的文件函数非常有用&#xff0c;类似 readfile()、 file() 和 file_get_contents()&#x…

网课查题系统搭建-查题校园题库

网课查题系统搭建-查题校园题库 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查题校园题库&#xff1a;查题校园题库后台&…

【C++笔记】第十九篇 多态

C的多态 1. 多态简介 ① 多态是C面向对象三大特性之一。 ② 多态分为两类&#xff1a; 静态多态&#xff1a;函数重载和运算符重载属于静态多态&#xff0c;复用函数名。动态多态&#xff1a;派生类和虚函数实现运行时多态。 ③ 静态多态和动态多态区别&#xff1a; 静态…

【源码分析】Spring中的设计模式——Context与Factory的关系

省流助手 两个类都实现了同一个接口&#xff0c;但是其中一个类对接口的实现是通过调用另一个类的接口实现来实现的&#xff0c;这就是静态代理模式(也可以说是装饰器模式&#xff0c;这俩区别不大) 这个例子中就是AbstractBeanFactory和AnnotationConfigApplicationContext都…

电子签批板那个品牌好用?国产柜台电子签名板推荐

如今已正式迈入数字时代&#xff0c;电子合同、电子签名不再新奇&#xff0c;各行各业对电子签名呈现出多元化的细分需求&#xff0c;应用场景也更加广泛。目前通信、银行、保险、酒店、政务等有柜台业务服务的领域大多都已配备了电子签字板用以替代传统纸张业务办理流程。加上…

First time to know JAVA

文章目录前言1.JAVA语言概述1.1 JAVA是什么&#xff1f;1.2 JAVA语言的重要性1.3 JAVA语言的发展简史1.4 JAVA语言的特性2.初识JAVA的main方法2.1 main方法示例2.2 运行JAVA程序3.JAVA中的注释3.1 JAVA注释的基本规则3.2 JAVA注释规范4.初始JAVA中的标识符5.初始JAVA中的关键字…