多线程编程(pthread和c++11 thread)以及c++11实现的线程池

笔记来源:

多线程和线程同步-C/C++_哔哩哔哩_bilibili

31.Thread 创建线程_哔哩哔哩_bilibili ->49.线程池和实现_哔哩哔哩_bilibili

基于C++11实现的异步线程池【C/C++】_哔哩哔哩_bilibili

爱编程的大丙 - 知识分享

多线程编程(pthread以及c++11 thread)

多线程基础知识

1 进程和线程

  • 什么是进程

    进程:进程是系统进行资源分配和调度的基本单位。当你启动一个程序时,操作系统会为这个程序创建一个进程,每个进程都有自己独立的地址空间、数据栈及其他辅助数据。进程之间相互独立,彼此之间不能直接共享内存这个被分配的地址空间不是物理内存空间,而是虚拟地址空间。

  • 什么是线程

    线程:线程是程序执行的最小单元,每个线程在自己的控制流中运行,拥有自己的程序计数器、栈和局部变量,各自有独立的栈空间和线程控制块TCB。**多个线程属于同一进程时,它们可以共享该进程的资源(如内存和打开的文件),并且通过共享内存进行更高效的通信。**操作系统通常根据线程执行的需求来进行调度,因此线程是OS调度的最小单位。

  • 进程和线程有什么关系

    1. 每个进程process都有相应的线程thread,进程是线程的容器
    2. 进程是资源分配的最小单位,而线程是调度执行的最小单位
    3. 进程有独立的地址空间,而线程没有,同一进程的线程共享本进程的地址空间

    总结来说,。因为同一进程中的多个线程可以并发执行,进行更快的上下文切换和资源共享。这样的设计使得操作系统能够在多任务和高并发环境中更高效地管理程序执行。

  • 线程的特点

    1. 轻量级与进程相比线程的创建和销毁成本较低
    2. 共享资源同一进程内的线程共享进程的地址空间和全局变量,使得线程之间的通信更加便捷,但也带来了数据同步和互斥的问题
    3. 并发执行多个线程可以并发执行,提升了程序的执行效率。由于线程的执行顺序和速度受到操作系统调度策略和硬件性能的影响,因此线程的执行结果是不可控的
    4. 独立调度线程是调度执行的基本单位,在多线程操作系统中,调度器根据线程的优先级、状态等因素来决定线程的调度顺序和执行时间
    5. 系统支持操作系统通常提供了对线程操作的API接口

2 多线程编程

  • 是什么

    在一个程序中创建多个线程并发的执行,不同的线程负责不同的任务

  • 为什么

    1. 充分利用CPU资源线程的执行可以充分利用CPU的多核或者多处理器资源,实现线程的并行执行,从而提高程序的执行效率
    2. 提高程序的响应速度将占用时间长的任务放到后台去执行,使得响应用户请求的线程能够尽快处理完成,缩短响应时间
    3. 便于程序设计和维护每个线程负责特定的任务
    4. 可以在不增加系统资源消耗的情况下提高程序的执行效率

3 线程与CPU执行关系

  1. 线程是CPU调度的最小单位,操作系统根据一定调度算法,将CPU的执行时间分配给各个线程,使它们能够并发执行

  2. CPU核数与多线程

    单核CPU多个线程并发执行,CPU会在不同线程之间快速切换,每次只执行一个线程的一部分,这种技术被称为时间片轮转Round Robin

    多核CPU可以实现并行执行,每个线程被分配给不同CPU核心去执行

  3. CPU时间片的分配操作系统根据一定调度算法来决定线程的执行方式,从而决定CPU时间片的分配

  4. 上下文切换:当CPU从一个线程切换到另一个线程时,需要保存当前线程的状态(上下文),并加载下一个线程的状态。这个过程中涉及的操作会消耗时间,因此频繁的上下文切换会影响性能

并行,并发和串行

并行多个任务同时执行

并发多个任务你先执行一部分,我后执行一部分的串行执行

串行多个任务按顺序一个一个执行

(通过下面的图进行举例)

first task完成需要10个时间片

second task完成需要15个时间片

third task完成需要5个时间片

0 5 10 15 20 25 30 first task first task second task third task first task 1 second task 1 second task third task 1 second task 2 second task 3 third task first task 2 串行 并行 并发 并行,并发和串行

4 线程的生命周期

  1. 新建状态(New):当线程对象被创建时,线程处于新建状态。此时线程尚未开始运行,也没用分配任何资源
  2. 就绪状态(Ready):当线程调用 start() 方法后,它进入就绪状态,准备好等待被操作系统分配 CPU 时间
  3. 运行状态(Running):当线程获得 CPU 时间片后,进入运行状态,直到任务完成或者遇到阻塞条件
  4. 阻塞状态(Blocked):线程执行的过程中遇到某些阻塞条件(如等待锁、IO操作完成)进入阻塞状态,此时,线程暂停执行,并且释放CPU资源,直到阻塞条件消失并且重新获得CPU时间片
  5. 死亡状态(Dead):当线程完成其任务,或者因未处理的异常终止时,线程进入死亡状态,该线程的资源被回收,生命周期结束

5 主线程和子线程

  • 主线程是程序启动时默认创建的线程

  • 子线程子线程是由主线程或其他线程创建的线程

  • 关系

    1. 创建关系主线程是程序启动时默认创建的线程

    2. 生命周期管理主线程负责管理子线程的生命周期。主线程可以启动、阻塞、唤醒或终止子线程。子线程的结束不一定会影响主线程,但主线程的结束通常会导致整个程序的退出,尤其是当子线程不是守护线程时

    3. 资源共享主线程和子线程可以共享同一进程中的资源

    4. 通信与同步主线程与子线程之间可以通过共享变量、方法调用、线程间通信机制进行数据传递和任务协调

    5. 执行顺序主线程和子线程是并发执行的,在某些情况下,主线程可能需要等待子线程完成,使用 join() 方法就可以实现这一目的

pthread

1 安装pthread库

Linux貌似自带这个库,windows需要自己安装这个

Visual Studio (2022)安装配置pthread.h多线程库_pthread.h库怎么安装?-优快云博客

其中的找不到pthreadVC2.dll报错可能需要使用pthread.h库中的相关函数时才会出现,所以把所有步骤都操作一遍就行

#include<pthread.h>//包含头文件

2 多线程操作

2.1 线程的创建

  • 获取线程id每一个线程都有一个唯一的id,id的类型为pthread_t,这个id是一个带有unsigned int32位无符号整数的结构体
typedef struct {
    void * p;                   /* Pointer to actual object */
    unsigned int x;             /* Extra information - reuse count etc */
} ptw32_handle_t;

typedef ptw32_handle_t pthread_t;
  • 如果想要获取其id可以调用以下函数:

    • pthread_t pthread_self (void);
printf("child thread: %lu\n", pthread_self());//可以使用但会报警告
  • 创建线程在一个进程中调用线程创建函数,就可以得到一个子线程,需要给每一个创建出来的线程一个指定的处理函数,否则这个子线程无法正常工作

    int pthread_create (pthread_t * tid,const pthread_attr_t * attr,void *(*start) (void *),void *arg);

    • 参数

      • tid指向一个pthread_t类型的指针,用于储存新创建线程的id

      • attr指向线程属性对象的指针,可以为NULL,表示默认属性

      • start指向一个函数的指针,也是作为这个新子线程的起始程序,必须接受一个void*类型的参数并且返回一个void*类型的值

        void*类型是一种泛型可以接受任意类型的数据,如链表,结构体…

      • arg被传输给start类型的参数,可以为NULL,表示无

    • 返回值:线程创建成功返回0,创建失败返回对应的错误号

函数指针

//这是一个自定义函数指针
typedef 参数类型1 (* fun_ptr)(参数类型2, 参数类型3...);
//参数类型1:指这个函数指针指向函数的返回值类型
//参数类型2,参数类型3...:指这个函数指向函数的参数列表类型

//e.g.
typedef int (*fun_ptr)(int,int);

int add(int a, int b) {
 return a + b;
}

fun_ptr func;
func = add;
func(1, 1);//2

实现了一个主线程以及其创建出的子线程,主线程和子线程都在循环打印数字

#include <iostream>
#include<pthread.h>
#include <Windows.h>
using namespace std;

void* callback(void* arg) {

	for (int i = 0; i < 100; i++) {
         //不使用cout的原因:可能会使输出不完整,比如只输出了main thread:
         //然后被其他线程抢走时间片,直到下一次该线程抢到时间片才能输出i以及换行符
         //cout << "main thread: " << i << endl;
		printf("child thread: %d\n", i);
	}
	//printf("child thread: %lu\n", pthread_self());
	printf("child thread: %lu\n", GetCurrentThreadId());

	return NULL;
}

int main() {
	pthread_t tid;
	pthread_create(&tid, NULL, callback, NULL);
	for (int i = 0; i < 20; i++) {
		printf("main thread: %d\n", i);
	}

	printf("main thread: %lu\n", GetCurrentThreadId());

	return 0;
}

运行结果如下(由于是多线程,所以说每一次的结果都不一样是正常的,但是还是有规律可循)

请添加图片描述

  1. 主线程和子线程是并发执行的,谁先抢到时间片谁先执行,执行完后进行下一轮时间片抢占

  2. 还有一些子线程输出是因为,打印主线程id和return 0;之间也有一段时间,这个时间片被子线程抢到了,等到主线程再一次抢到时间片才能执行return 0;,也有可能主线程之后抢不到时间片,等到子线程结束主线程轮到主线程

2.2 线程的退出

在编写多线程程序的时候,如果想要线程退出并且不影响其他的线程的运行

一般是对主线程使用,使得主线程退出但不会导致虚拟地址空间的释放

  • void pthread_exit (void *value_ptr);

    • value_ptr

      与线程回收函数pthread_join()搭配使用

      一个指向任意类型的指针,用于将当前子线程的数据返回给其主线程,可以为NULL表示无

void* callback(void* arg) {
	for (int i = 0; i < 100; i++) {
		printf("child thread: %d\n", i);
	}
	printf("child thread: %lu\n", GetCurrentThreadId());
	return NULL;
}

int main() {
	pthread_t tid;
	pthread_create(&tid, NULL, callback, NULL);
    
	printf("main thread: %lu\n", GetCurrentThreadId());

	pthread_exit(NULL);

	return 0;
}

运行后可以发现,主线程早早的输出完主线程id后并没有立即结束程序,而是每次都等待子线程执行完毕后才结束程序的进程

2.3 线程的回收

线程和进程一样,子线程退出的时候其内核资源需要由主线程回收

  • int pthread_join (pthread_t thread,void **value_ptr);等待回收

    这是一个阻塞函数,会阻塞调用线程,直到指定的线程结束

    一个回收函数只能回收一个子线程,多个子线程就需要回收多次

    • 参数
      • thread指定等待结束的线程
      • value_ptr用于接收线程退出时返回的值
    • 返回值:线程回收成功返回0,回收失败返回错误号
回收子线程的数据
  • 使用堆区变量,线程退出,线程回收

    通过子线程退出函数带出数据的地址,再由主线程回收函数接收子线程退出带出的地址

    💡由于分配在栈区的数据在子线程结束后会被释放,所以传递的数据需要被分配在堆区中(还不如直接访问堆区数据)

struct test {
	int a;
	int b;
};
struct test t;//堆区数据

void* callback(void* arg) {
	for (int i = 0; i < 5; i++) {
		printf("child thread: %d\n", i);
	}
	printf("child thread: %lu\n", GetCurrentThreadId());

	t.a = 10;//写入数据
	t.b = 10;

	pthread_exit(&t);//子线程退出带出数据地址

	return NULL;
}

int main() {
	pthread_t tid;
	pthread_create(&tid, NULL, callback, NULL);

	printf("main thread: %lu\n", GetCurrentThreadId());

	void* ptr;//接收地址的参数
	pthread_join(tid, &ptr);//主线程回收函数接收地址

	struct test* t1 = (struct test*)ptr;

	printf("a = %d, b = %d\n", t1->a, t1->b);

	return 0;
}
  • 使用主线程栈,线程创建函数接收数据,线程回收阻塞

    主要是利用创建线程函数中的arg参数,和线程回收的阻塞功能

void* callback(void* arg) {
	for (int i = 0; i < 5; i++) {
		printf("child thread: %d\n", i);
	}
	printf("child thread: %lu\n", GetCurrentThreadId());

	struct test* t = (struct test*)arg;//子线程访问主线程的占内存
	t->a = 10;
	t->b = 10;
    
	return NULL;
}

int main() {
	pthread_t tid;
	struct test t1 = { 0 };//主函数栈创建
    
	pthread_create(&tid, NULL, callback, &t1);//主线程的栈内存传给子线程

	printf("main thread: %lu\n", GetCurrentThreadId());

	pthread_join(tid, NULL);//起到阻塞作用,等待子线程执行完毕
    //不能使用线程退出,相当于return 0;,不再向下执行打印操作
    
	printf("a = %d, b = %d\n", t1.a, t1.b);

	return 0;
}

2.4 线程的分离

有些情况,程序中的主线程也有自己的业务处理流程,如果调用线程回收函数只要子线程不退出主线程,主线程就会一直被阻塞,主线程的任务也就不能继续执行

在线程库函数中为我们提供了线程分离函数,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了

💡线程分离之后在主线程中使用pthread_join回收函数就回收不到该子线程资源了

  • int pthread_detach(pthread_t tid);
    • 参数
      • tid要分离的线程的线程标识符
    • 返回值:成功返回0,失败返回对应错误码
void* callback(void* arg) {
	for (int i = 0; i < 30; i++) {
		printf("child thread: %d\n", i);
	}
	printf("child thread: %lu\n", GetCurrentThreadId());

	return NULL;
}

int main() {
	pthread_t tid;
	pthread_create(&tid, NULL, callback, NULL);

	printf("main thread: %lu\n", GetCurrentThreadId());

	pthread_detach(tid);//线程分离
    
    pthread_exit(NULL);//主线程退出

	//return 0;
}
💡输出不一致的情况
  1. 当只写了pthread_exit(NULL);或者是先pthread_exit(NULL);return 0;

    我们发现尽管主线程和子线程已经分离,但程序总能将子线程完全跑完后结束

请添加图片描述

  1. 只写了return 0;

    我们却发现子线程现在很难输出完所有的值

请添加图片描述

从上面的情况我们可以总结出两个问题

  1. 明明主线程和子线程已经完全分离,为什么情况1中的子线程每一次都能输出完全呢?

  2. 为什么1和2的情况会有所差别呢?

  3. 进程并没有结束,而是等待所有的线程结束,进程才会结束

正常来说,主线程和进程的生命周期应该是相同的,但是如果使用pthread_exit来结束线程,主线程可以显式地结束,但进程不会立即终止。相反,操作系统会允许其他线程(包括分离的和非分离的线程)继续运行,直到所有非分离的子线程结束,主线程结束,但是进程并没有结束,而是等待其他的线程执行完毕

  1. 1中的进程并没有因为主线程的结束而结束,而2中的进程在主线程结束后被强制结束

我们可以发现,原因其实和pthread_exitreturn 0;有关

线程退出和return 0;都能使主线程退出,但是退出产生的效果是不相同的

  • pthread_exit主线程可以显式地结束,但进程不会立即终止。主线程会等到所有非分离的子线程结束而结束,但进程会等到所有线程结束而结束

  • return 0;会强制使整个进程结束,无论是否还有线程正在运行

因此,在多线程编程中需要选择合适的主线程结束方式pthread_exitreturn 0;

pthread_exit(); 则允许其他非分离线程继续运行,使得主线程的结束不会影响正在执行的其他线程,适合需要确保其他线程完成的场景

如果你只使用 return 0;,并且没有确保子线程执行完毕,那么未分离的子线程可能不会完成其任务

2.5 其他线程函数

  • 线程取消:发送一个请求到指定的线程B,以便它能够终止自身的执行

    取消条件:在线程B中进程一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。

    (比如说printf间接的系统调用)

    int pthread_cancel (pthread_t thread);

    • 参数
      • tid要取消的线程的线程标识符
    • 返回值:成功返回0,失败返回对应错误码
  • 线程ID比较:用于比较两个线程ID,以确定它们是否指向同一个线程

    int pthread_equal(pthread_t t1, pthread_t t2);

    • 参数
      • pthread_t t1:第一个线程的ID
      • pthread_t t2:第二个线程的ID
    • 返回值
      • 如果两个线程ID相等,返回一个非零值(通常是 1)
      • 如果两个线程ID不相等,返回 0

3 线程同步

所谓的同步并不是多个线程同时对内存访问,而是按照先后顺序依次进行的

3.1 为什么要进行线程同步以及同步方式

  • 为什么要进行线程同步

两个线程一起进行number累加到100的操作

该程序刻意进行了一些操作,更容易再现数据混乱的情况

  • 用另一个变量接收number进行自增再传回去
  • 在特定地方使用sleep函数
#include <iostream>
#include<pthread.h>
#include <Windows.h>
using namespace std;

int number = 0;

void* callback1(void* arg) {
	for (int i = 0; i < 50; i++) {
		int cur = number;//增加写回number的操作
		cur++;
		Sleep(10);
         //1.希望在数据写回number前时间片结束,被其他线程抢占
         //2.延长程序运行时间,增加抢占时间片的次数
		number = cur;
		printf("child thread1: %d\n", cur);
	}
	return NULL;
}

void* callback2(void* arg) {
	for (int i = 0; i < 50; i++) {
		int cur = number;
		cur++;
		number = cur;
		printf("child thread2: %d\n", cur);
		Sleep(5);
         //延长程序运行时间,增加抢占时间片的次数
	}
	return NULL;
}

int main() {
	pthread_t tid1, tid2;

	pthread_create(&tid1, NULL, callback1, NULL);
	pthread_create(&tid2, NULL, callback2, NULL);

	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);

	return 0;
}

可以看的结果很难累加到100

请添加图片描述

CPU对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被CPU处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件IO操作写入到磁盘中。

在测试程序中两个线程共用全局变量number当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到CPU进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。

如果线程A执行这个过程期间就失去了CPU时间片,线程A被挂起了最新的数据没能更新到物理内存。线程B变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去CPU时间片挂起。线程A得到CPU时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程B已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。

作者: 苏丙榅
链接: 线程同步 | 爱编程的大丙
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 线程同步方式
    • 互斥锁
    • 读写锁
    • 条件变量
    • 信号量

共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者是堆区变量,这些变量对应的共享资源也被称为临界资源

临界资源(Critical Resource)是指在多线程或多进程环境中,可能被多个线程或进程同时访问的资源,这些资源的访问必须是互斥的,以防止数据竞争和不一致的状态

找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区

  • 线程同步的思路

    1. 确定好临界区(临界区越小越好,减少临界区粒度

    2. 在临界区代码之前添加锁函数,对临界区加锁

      哪个线程先执行到这句代码,就会把锁锁上,其他线程就只能阻塞在这把锁上

    3. 在临界区代码之后添加解锁函数,对临界区解锁

      上这把锁的线程出临界区后执行解锁代码,临界区前的锁函数解锁,重新开始抢锁

    4. 通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问

3.2 互斥锁

一般情况下,一个共享资源对应一把互斥锁,与线程的个数无关

  • 创建一把互斥锁
pthread_mutex_t mutex;
  • 互斥锁的创建和销毁

    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

    使用互斥锁前请务必初始化它否则可能导致未定义的行为

    • 参数
      • mutex指向要初始化互斥锁的指针
      • attr互斥锁的属性一般使用默认NULL
    • 返回值
      • 成功返回0,失败返回对应错误码

    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    试图销毁一个仍然被锁定的互斥锁会导致未定义的行为

    • 参数
      • mutex指向要销毁互斥锁的指针
    • 返回值
      • 成功返回0,失败返回对应错误码
  • 互斥锁使用

  • 上锁

    int pthread_mutex_lock(pthread_mutex_t *mutex);

    • 参数

      • mutex:指向互斥锁对象的指针,该互斥锁在调用之前必须已经初始化
    • 返回值

      • 成功时返回0失败时返回错误码
  • 尝试上锁

    int pthread_mutex_trylock(pthread_mutex_t *mutex);

    不会使调用线程阻塞,如果当前互斥锁被其他线程锁定,它会立刻返回

    可以通过判断上锁情况对不同数据进行处理

    • 参数
      • mutex:指向互斥锁对象的指针,该互斥锁在调用之前必须已经初始化
    • 返回值
      • 成功时返回0失败时返回错误码
  • 解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

    • 参数
      • mutex:指向互斥锁对象的指针,该互斥锁必须是上锁线程所持有的
    • 返回值
      • 成功时返回0失败时返回错误码

继续先前的累加操作

#include <iostream>
#include<pthread.h>
#include <Windows.h>
using namespace std;

int number = 0;
pthread_mutex_t mutex;//创建互斥锁

void* callback1(void* arg) {
	for (int i = 0; i < 50; i++) {
		pthread_mutex_lock(&mutex);//对number操作前上锁

		int cur = number;
		cur++;
		Sleep(10);
		number = cur;
		printf("child thread1: %d\n", cur);
		
		pthread_mutex_unlock(&mutex);//对number操作后解锁
	}
	return NULL;
}

void* callback2(void* arg) {
	for (int i = 0; i < 50; i++) {
		pthread_mutex_lock(&mutex);//对number操作前上锁

		int cur = number;
		cur++;
		number = cur;
		printf("child thread2: %d\n", cur);
		Sleep(5);

		pthread_mutex_unlock(&mutex);//对number操作后解锁
	}
	return NULL;
}

int main() {
	pthread_t tid1, tid2;
	pthread_mutex_init(&mutex, NULL);//互斥锁初始化

	pthread_create(&tid1, NULL, callback1, NULL);
	pthread_create(&tid2, NULL, callback2, NULL);

	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);

	pthread_mutex_destroy(&mutex);//互斥锁销毁
	pthread_exit(NULL);
}

请添加图片描述

3.3 死锁

由于使用锁时操作不当,导致所有线程都被阻塞,并且线程的阻塞是无法被解开的

产生的原因

  • 加锁之后忘记解锁
  • 重复加锁
  • 程序中有多把锁,随意加锁导致互相被阻塞

3.4 读写锁

提升读取时的执行效率

在读操作的时候可以实现并行访问,但在写操作时依旧是串行访问

锁定的状态分为读操作锁定写操作锁定

如果使用读写锁锁定了某种操作,需要等待解锁才能进行另一种操作

pthread_rwlock_t rwlock;//定义一个读写锁

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);//读写锁初始化

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//读写锁销毁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读操作锁定

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//尝试读操作锁定

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写操作锁定

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//尝试写操作锁定

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁

读操作访问已经锁定读操作的读写锁,依然可以锁定成功

  • 参数说明

    • rwlock:指向要初始化的读写锁对象的指针

    • attr:用于指定读写锁的属性,一般可以传入 NULL,表示使用默认属性

  • 返回值

    • 如果函数成功,则返回0,如果函数失败,则返回错误码
pthread_rwlock_t rwlock;//定义一个读写锁

void* ReadThread(void* arg) {
	srand((unsigned)time(NULL));
	for (int i = 0; i < 50; i++) {
		pthread_rwlock_rdlock(&rwlock);//读锁
		printf("read thread %lu: %d\n", GetCurrentThreadId(), number);
		pthread_rwlock_unlock(&rwlock);//解锁
		Sleep(rand() % 10);
	}
	return NULL;
}

void* WriteThread(void* arg) {
	for (int i = 0; i < 50; i++) {
		pthread_rwlock_wrlock(&rwlock);//写锁
		int cur = number;
		cur++;
		number = cur;
		printf("write thread %lu: %d\n", GetCurrentThreadId(), number);
		pthread_rwlock_unlock(&rwlock);//解锁
		Sleep(10);
	}
	return NULL;
}

int main() {
	pthread_t read_tid[5], write_tid[2];

	pthread_rwlock_init(&rwlock, NULL);//读写锁初始化

	for (int i = 0; i < 5; i++) {
		pthread_create(&read_tid[i], NULL, ReadThread, NULL);
	}
	for (int i = 0; i < 2; i++) {
		pthread_create(&write_tid[i], NULL, WriteThread, NULL);
	}

	for (int i = 0; i < 5; i++) {
		pthread_join(read_tid[i], NULL);
	}
	for (int i = 0; i < 2; i++) {
		pthread_join(write_tid[i], NULL);
	}

	pthread_rwlock_destroy(&rwlock);//读写锁销毁

	pthread_exit(NULL);
}

3.5 条件变量

与互斥锁配合使用,对加锁后符合条件的线程进行阻塞

一般是在互斥锁作用范围内进行条件变量的使用

条件需要自己设置

被阻塞的线程的线程信息会被记录在条件变量中,以便后续的解锁

  • 条件变量的定义以及初始化和销毁
pthread_cond_t cond;//定义一个自定义变量

int pthread_cond_init (pthread_cond_t * cond,const pthread_condattr_t * attr);

int pthread_cond_destroy (pthread_cond_t * cond);
  • 条件变量的阻塞功能
    • 当线程执行到该函数时则会被阻塞,如果线程已经对mutex上锁,那么该函数会将这把锁打开,以防死锁
    • 当该线程被唤醒即阻塞解除时,又会重新将mutex加上锁,该线程继续向下访问临界区
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//可以看到需要传入一把互斥锁
int pthread_cond_timedwait (pthread_cond_t * cond,pthread_mutex_t * mutex,const struct timespec *abstime);//只将线程阻塞固定时间

//表示1970.1.1 0:0:0 到当前的总秒数
struct timespec
{
    //秒和纳秒
    time_t tv_sec;  // Seconds - >= 0
    long   tv_nsec; // Nanoseconds - [0, 999999999]
};

//具体使用,即怎么设置abstime
time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s
  • 条件变量唤醒阻塞线程
// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

参数

  • cond条件变量的地址

  • attr条件变量属性, 一般使用默认属性, 指定为NULL

  • mutex互斥锁地址

  • abstime指定时间

返回值

  • 返回0表示操作成功
  • 返回-1表示操作失败
📖生产者消费者模型

创建五个生产者线程和五个消费者线程对有限的10个空间进行访问

#include<iostream>
#include<pthread.h>
#include<Windows.h>

pthread_mutex_t mutex;
pthread_cond_t cond;

typedef struct Node
{
	static int count;
	int number;
	struct Node* Next;
};

int Node::count = 0;
Node* headptr = NULL;

void* Producer(void* arg)
{
	while (true)
	{
		pthread_mutex_lock(&mutex);
		//使用while以防多个线程被释放后导致访问越界
		while (Node::count == 10) {//当空间满则阻塞生产者的生产
			pthread_cond_wait(&cond, &mutex);//在互斥锁作用区间内使用条件变量
		}

		Node* node = new Node();//生产操作
		node->count++;
		node->number = node->count;
		node->Next = headptr;
		headptr = node;
		printf("Producer %lu produce %d. \n", pthread_self(), node->number);
		pthread_mutex_unlock(&mutex);

		pthread_cond_broadcast(&cond);//唤醒被阻塞的线程

		Sleep(1000);
	}
	return NULL;
}

void* Consumer(void* arg)
{
	while (true)
	{
		pthread_mutex_lock(&mutex);
		//使用while以防多个线程被释放后导致访问越界
		while (Node::count == 0) {//当空间空则阻塞消费者的消费
			pthread_cond_wait(&cond, &mutex);//在互斥锁作用区间内使用条件变量
		}

		Node* temp = headptr;//消费操作
		headptr->count--;
		headptr = headptr->Next;
		printf("Consumer %lu consume %d. \n", pthread_self(), temp->number);
		delete(temp);
		pthread_mutex_unlock(&mutex);

		pthread_cond_broadcast(&cond);//唤醒被阻塞的线程

		Sleep(1000);
	}
	return NULL;
}

int main()
{
	pthread_mutex_init(&mutex, NULL);//互斥锁初始化
	pthread_cond_init(&cond, NULL);//条件变量初始化

	pthread_t producer[5], consumer[5];

	for (int i = 0; i < 5; i++)
	{
		pthread_create(&producer[i], NULL, Producer, NULL);
	}

	for (int i = 0; i < 5; i++)
	{
		pthread_create(&consumer[i], NULL, Consumer, NULL);
	}

	for (int i = 0; i < 5; i++)
	{
		pthread_join(producer[i], NULL);
		pthread_join(consumer[i], NULL);
	}

	pthread_mutex_destroy(&mutex);//互斥锁销毁
	pthread_cond_destroy(&cond);//条件变量销毁

	pthread_exit(NULL);
}

注意点

  1. 条件变量的阻塞需要在互斥锁作用范围内使用,因为条件变量的条件判断涉及到对共享资源的访问,如果置于互斥锁前进行条件变量的阻塞可能会导致虚假唤醒
  2. 条件判断需要使用while防止释放多条线程后导致访问越界

3.6 信号量

与锁配合使用,在进行加锁前放行固定数量的线程,阻塞多余的线程

信号量和条件变量的异同

信号量和条件变量相似,都是限定一定数量线程去访问共享资源,但是条件变量通过唤醒和条件的限制实现,而信号量是通过自带的资源量value的数量控制以及其增加和减少实现

又因为条件变量的条件判断通常又涉及到共享资源的访问,因而需要在锁内进行阻塞函数的使用,而信号量因为自带资源量value不需要在锁内使用,但是由于锁内使用缺少对锁外线程的判断容易导致死锁,因而只能在锁外进行信号量阻塞函数的使用

  • 需要包含头文件
#include<semaphore.h>//注意是带.h的头文件
  • 创建、初始化以及销毁
sem_t semaphore;

int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_destroy(sem_t *sem);

参数

  • pshared

    • 0表示线程同步
    • 非0表示进程同步
  • value

    • 初始化该信号量持有的资源数量,大于等于0的正整数
  • 信号量的阻塞函数

    当函数调用的信号量资源数大于0时,每当一条线程执行该函数,该信号量所持有资源量-1,放行一条线程,当信号量所持有资源量为0时阻塞之后访问的线程

int sem_wait(sem_t *sem);//资源耗尽进行阻塞

int sem_trywait(sem_t *sem);//不阻塞,资源耗尽返回-1

int sem_timedwait(sem_t *sem, const struct timespec *abstime);//资源耗尽后阻塞一段时间
  • 解除阻塞函数

    调用该函数使得目标信号量所持有资源量+1

int sem_post(sem_t *sem);
  • 获取信息量所持有资源量
int sem_getvalue(sem_t *sem, int *sval);//sval作为该函数返回值,存储该信号量持有资源量

参数

  • sem信号量地址

返回值

  • 成功返回0
  • 失败返回-1
📖生产者消费者模型

创建五个生产者线程和五个消费者线程对有限的10个空间进行访问

#include<iostream>
#include<pthread.h>
#include<Windows.h>
#include<semaphore.h>

pthread_mutex_t mutex;

sem_t producer_semaphore;//创建生产者信号量
sem_t consumer_semaphore;//创建消费者信号量

typedef struct Node
{
	static int count;
	int number;
	struct Node* Next;
};

int Node::count = 0;
Node* headptr = NULL;

void* Producer(void* arg)
{
	while (true)
	{
         //生产者生产一个资源,因为位置有限,可以生产的总资源数-1
		sem_wait(&producer_semaphore);//放行一定数量的线程访问共享资源

		pthread_mutex_lock(&mutex);//加锁
		Node* node = new Node();
		node->count++;
		node->number = node->count;
		node->Next = headptr;
		headptr = node;
		printf("Producer %lu produce %d. \n", pthread_self(), node->number);
		pthread_mutex_unlock(&mutex);//解锁

		sem_post(&consumer_semaphore);//生产者生产一个资源,消费者多一个资源可消费+1

		Sleep(1000);
	}
	return NULL;
}

void* Consumer(void* arg)
{
	while (true)
	{
         //消费者消费一个资源,可以消费的总资源数-1
		sem_wait(&consumer_semaphore);//放行一定数量的线程访问共享资源

		pthread_mutex_lock(&mutex);//加锁
		Node* temp = headptr;
		headptr->count--;
		headptr = headptr->Next;
		printf("Consumer %lu consume %d. \n", pthread_self(), temp->number);
		delete(temp);
		pthread_mutex_unlock(&mutex);//解锁

		sem_post(&producer_semaphore);//消费者消费一个资源,因为位置有限,生产者可以生产的资源总数+1

		Sleep(1000);
	}
	return NULL;
}

int main()
{
	pthread_mutex_init(&mutex, NULL);
	sem_init(&producer_semaphore, 0, 10);//初始化生产者资源量
	sem_init(&consumer_semaphore, 0, 0);//初始化消费者资源量

	pthread_t producer[5], consumer[5];

	for (int i = 0; i < 5; i++)
	{
		pthread_create(&producer[i], NULL, Producer, NULL);
	}

	for (int i = 0; i < 5; i++)
	{
		pthread_create(&consumer[i], NULL, Consumer, NULL);
	}

	for (int i = 0; i < 5; i++)
	{
		pthread_join(producer[i], NULL);
		pthread_join(consumer[i], NULL);
	}

	pthread_mutex_destroy(&mutex);
	sem_destroy(&producer_semaphore);//销毁
	sem_destroy(&consumer_semaphore);

	pthread_exit(NULL);

}

注意点

  1. 信号量对线程的阻塞函数需要在互斥锁作用范围前使用,如果在作用范围内使用,否则当资源数为零时,已经把互斥锁上锁的线程将被阻塞在该函数上,由于互斥锁已经上锁,其他线程访问不了共享资源,导致死锁

thread c++11

请添加图片描述

1 多线程操作

1.1 创建一个线程

包含thread头文件

使用std::thread类来创建一个对象并传递一个可调用对象作为线程的执行体

#include<iostream>
#include<thread>

void function() {
	std::cout << "hello thread" << std::endl;
}

int main() {
	std::thread my_thread(&function);//创建子线程
	my_thread.join();//阻塞主线程,等待该子线程结束
	return 0;
}

1.2 可调用对象

  • 函数指针(存在隐式转换可以省略&)

  • 成员函数指针因为成员函数的调用需要一个类的实例作为上下文。直接将成员函数传递给 std::thread 会导致编译错误

    解决方案

    • 使用对象指针
    • 使用lambda表达式
  • lambda表达式(匿名函数)

  • 函数对象(也称仿函数functor)

  • 绑定对象(通过std::bind创建,需要引入functional头文件)

//函数指针
void function() {
	std::cout << "hello thread" << std::endl;
}
std::thread my_thread(&function);
std::thread my_thread(function);//存在隐式转换可以省略

//成员函数指针
class MyClass {
public:
    void function() {
		std::cout << "hello thread" << std::endl;
	}
};
MyClass my_class;
std::thread my_thread(&MyClass::function, &my_class);//1使用对象指针
void test()//2使用lambda表达式
{
	MyClass my_class_;//不能是全局变量
	std::thread my_thread([&my_class_]() {
		my_class_.function();
		});
}

//lambda表达式
std::thread my_thread([]() {std::cout << "hello thread" << std::endl; });

//函数对象
class MyClass {
public:
	void operator()() {
		std::cout << "hello thread" << std::endl;
	}
};
MyClass my_class;
std::thread my_thread(my_class);

//绑定对象
#include<functional>//引入头文件
void function(int a, int b) {
	std::cout << "hello thread " << a << " " << b << std::endl;
}
auto bound_function = std::bind(function, 1, 2);
std::thread my_thread(bound_function);

函数对象一个重载了函数调用操作符operator()的类的实例,使得函数对象不仅具有普通函数的功能还拥有状态信息,并且可以在不同的调用之间保持这些状态信息

lambda表达式一般形式如下

[capture-list] (parameter-list) -> return-type { function-body }
  • capture-list捕获列表用于捕获外部作用域中的变量,可以为空
    • [x]按值捕获
    • [&x]按引用捕获
    • [=] or [&]默认按值捕获或者按引用捕获所有外部变量
    • [=, &x] [&, x] ...混合捕获
  • parameter-list参数列表定义函数的输入参数,可以为空
  • return-type返回值类型(如果函数体只有一条return语句,编译器可以自动推导返回类型,此时可以省略返回值类型)
  • function-body函数体
int a = 10;
int b = 20;

//atuo自动推导数据类型
auto fun = [=, &b](int c, int d) -> int {
	//a = 20;//按值捕获,内部不可以修改
	b = 10;//按引用捕获,修改后外部作用域的值也会改变
	std::cout << "hello thread 1 " << b << std::endl;
	return a + b + c + d;
	};

std::cout << fun(1, 2) << " " << b << std::endl;//调用lambda表达式创建的函数
//hello thread 1 10
//23 10

[]() { std::cout << "hello thread 2" << std::endl; }();//lambda表达式后直接加()进行调用
//hello thread 2

1.3 传递参数

  • 通过值传递

    值被拷贝传递

void function(int a, int b) {
	std::cout << "hello thread " << a << " " << b << std::endl;
}

std::thread my_thread(function, 10, 20);//通过数值
int a = 10;
int b = 20;
std::thread my_thread(function, a, b);//通过变量名
  • 通过引用传递

    实际上在传递原始数据,意味着当它被修改后,该数据原本的值也将被修改

void function(int & a, int & b) {
	std::cout << "hello thread " << a << " " << b << std::endl;
}

int a = 10;
int b = 20;
std::thread my_thread(function, std::ref(a), std::ref(b));//需要使用std::ref

std::ref() 是 C++ 标准库中的一个函数模板,定义在 <functional> 头文件中。它的主要用途是创建一个 std::reference_wrapper 对象,用来包装一个引用,使其可以像值一样传递

reference_wrapper<_Ty> ref(reference_wrapper<_Ty> _Val) noexcept {
 return _Val;
}
//reference_wrapper模板类中有如下成员函数
operator _Ty&() const noexcept {//()运算符的重载
     return *_Ptr;
}
_Ty& get() const noexcept {//get()成员函数
     return *_Ptr;
}

//也就是说我们可以
std::reference_wrapper<int> wrapper_a = a;
std::reference_wrapper<int> wrapper_b = std::ref(b);

function(wrapper_a, wrapper_b);
function(wrapper_a.get(), wrapper_b.get());

std::thread my_thread(function, wrapper_a, wrapper_b);

//但是不可以这样
std::thread my_thread(function, wrapper_a.get(), wrapper_b.get());//报错
/*
当你将 int& 作为参数传递给 std::thread 时,会因为 std::thread 期望接收可调用对象和参数而导致不符合要求,线程中的参数会在其创建时被复制,因此,如果你只是传递引用,线程可能会在使用这些参数时引用已失效的对象
*/

1.4 this_thread

std::this_thread是cpp标准库中的一个命名空间,在该命名空间下的函数允许我们获取和操作当前线程的特定属性,如线程ID,使当前线程休眠等

  • 获取当前线程的ID

    std::this_thread::get_id()

    返回值

    • 返回一个std::thread::id类型的对象
std::thread::id thread_id = std::this_thread::get_id();
std::cout << "hello thread " << thread_id << std::endl;
  • 使当前线程休眠

    std::this_thread::sleep_for(const chrono::duration<_Rep, _Period>& _Rel_time)

    参数

    • _Rel_time需要一个const std::chrono::duration类型的参数
//需要引入chrono头文件
#include<chrono>
std::this_thread::sleep_for(std::chrono::hours(1));//小时
std::this_thread::sleep_for(std::chrono::minutes(1));//分钟
std::this_thread::sleep_for(std::chrono::seconds(1));//秒
std::this_thread::sleep_for(std::chrono::milliseconds(1));//毫秒
std::this_thread::sleep_for(std::chrono::microseconds(1));//微秒
std::this_thread::sleep_for(std::chrono::nanoseconds(1));//纳秒
  • 使当前线程放弃当前CPU时间片,让其他线程获得运行机会

    std::this_thread::yield()

1.5 join和detach

  • join

    • 当调用join方法后,本线程会被阻塞,直到调用join方法的线程执行完成
    • 确保子线程在主线程之前执行完成,并且将子线程的资源被正确的回收和清理
  • detach

    • 当调用detach方法后,调用该方法的线程会与本线程分离它将不在受到本线程的控制,主线程不需要也不能再对其进行join操作,因为它已经脱离了本线程的控制
    • 分离线程会被系统自动回收其资源
  • 如果主线程在子线程未结束的情况下尝试销毁其所对应的std::thread对象,而该对象又未被join或者detach则会导致程序异常中止

终止和中止的区别

  • 终止
    • 用户请求退出应用程序
    • 程序完成所有任务后自然结束
    • 程序检测到某个终止条件,如达到最大迭代次数
  • 中止
    • 程序遇到严重的运行时错误,无法继续执行
    • 程序调用 abort() 函数,通常是因为检测到不可恢复的错误
    • 操作系统或硬件故障导致程序崩溃

1.6 一个线程包含些什么

  1. 线程ID每个线程都有的唯一标识符
  2. 线程栈每个线程的私有栈空间,用于储存局部变量、函数调用时的参数和返回地址信息等。创建线程时分配,并在线程结束时释放
  3. 线程状态
  4. 线程上下文包含线程执行时需要的所以信息,如CPU寄存器内容、程序计数器PC值、栈指针、信号掩码等。当线程切换时被保存,以便在之后恢复执行时能够继续执行
  5. 线程函数包含线程需要执行的代码
  6. 线程优先级
  7. 线程属性设置线程的一些属性,如栈大小、安全属性
  8. 线程同步原语协调线程的执行,帮助在访问共享资源时避免冲突和竞态条件,如互斥锁…

1.7 注意事项

  • 线程安全需要确保共享资源的访问是线程安全的
  • 资源管理动态分配的资源需要在不再使用后正确释放
  • 异常处理新线程中抛出的异常不会自动传播到创建该线程的线程

2 线程同步

通过一定机制来控制多个线程之间的执行顺序,以确保他们能够正确地访问和修改共享资源

  • 线程同步方式
    • 互斥锁
    • 读写锁
    • 条件变量
    • 信号量
    • 原子操作
    • 栅栏(c++20)

2.1 线程同步机制

  1. 互斥锁

  2. 条件变量

  3. 信号量

  4. 原子操作原子操作是不可中断的操作,即在执行过程中不会被其他线程打断

    cpp11及以后版本提供了atomic头文件,包含了一系列原子操作的函数和类,可以更安全的更新共享数据,而无需使用互斥锁等同步机制

2.2 互斥锁mutex

  • 包含头文件
#include<mutex>
  • 定义一把互斥锁

    创建一个std::mutex对象

std::mutex my_mutex;
  • 加锁和解锁
my_mutex.lock();//加锁
/*
	访问共享资源的区域
*/
my_mutex.unlock();//解锁
四种不同的互斥锁
  • std::mutex

    • 最基本的互斥量类型
    • **一个线程不能连续的多次锁定同一个std::mutex**这是一种未定义行为,会导致死锁
    • 提供最基本的加锁,尝试加锁和解锁操作
    • 当一个线程锁定std::mutex时,任何尝试对该互斥量进行锁定的线程都将被阻塞,直到锁定该互斥量的线程进行解锁操作
  • std::recursive_mutex

    • 这是一个递归recursive(或可重入)互斥量

    • 它允许同一个线程连续的多次锁定同一个互斥量,相应的进行多少次锁定也需要进行多少次解锁

      内部通过一个计数器来实现该互斥锁的释放,加锁时进行+1,解锁时进行-1,当计数器减到0时锁才会真正被释放

    • 提供最基本的加锁、尝试加锁和解锁操作

    • 当加锁操作大于解锁操作时,加锁线程执行完所有操作后将会被阻塞,因为互斥锁并没有被释放,所以导致其他线程也被阻塞在加锁操作中,导致死锁

      当加锁操作小于解锁操作时,将涉及对未加锁线程进行解锁操作的未定义行为,当第一次进行该操作时程序将异常中止

  • std::timed_mutex

    • 这是一个带时限的互斥量

    • 除了提供基本的加锁、尝试加锁和解锁操作,还允许尝试在一定时间内锁定互斥量

      try_lock_for()指定时间段内尝试加锁

      try_lock_until()指定时刻前尝试加锁

      加锁成功返回true失败返回false,如果加锁失败,线程将不被继续阻塞而是继续执行,因而需要对加锁的情况进行判断,以进行不同的处理

      💡 加锁成功记得解锁操作,失败则不需要也不能进行解锁操作

  • std::recursive_timed_mutex

    • 这是一个递归且带时限的互斥量
    • 结合了recursive_mutextimed_mutex的特性

2.3 lock_guard

lock_guard是一个模板类,在<mutex>头文件中,用于管理互斥锁的生命周期

设计目的是为了提供一种简单的锁管理机制mutex

虽然也能使用recursive_mutex但是不推荐,可能导致难以调试的死锁问题

在初始化该模板类时进行加锁操作,在其析构函数中进行解锁操作

符合RAII风格

RAII风格

Resource Acquisition Is Initialization是C++中的一种编程范式,其核心思想是在对象的生命周期中管理资源。通过将资源的获取与对象的创建绑定在一起,并在对象销毁时自动释放资源,RAII有助于防止资源泄漏并简化代码

  • 该对象创建时分配资源
  • 当对象生命周期结束时释放资源
  • 异常安全即使抛出异常也能保证资源安全释放

例子:智能指针,容器,锁…

  • 使用

    std::lock_guard<互斥锁类> lock(互斥锁对象);

    std::lock_guard<互斥锁类> lock(互斥锁对象, std::adopt_lock);

    参数

    • std::adopt_lock是一个标签类型,表示你已经手动锁定互斥锁并且希望lock_guard接管这个锁的管理,这意味着它会负责后续锁的解锁操作

      💡 你不能接管一把已经被lock_guard初始化的互斥锁

std::mutex my_mutex;

std::lock_guard<std::mutex> lock(my_mutex);
std::mutex my_mutex;

my_mutex.lcok();//手动锁定

std::lock_guard<std::mutex> lock(mutex, std::adopt_lock);
  • 特点

    • RAII

    • 简单易用,只需创建lock_guard对象,无需继续调用上锁和解锁操作

    • 异常安全

    • 不可复制和移动lock_guard类中的拷贝构造函数和赋值运算符被禁用

      确保该类对象不可被拷贝,避免了多个lock_guard对象同时管理同一个互斥锁而导致错误行为

      lock_guard(const lock_guard&)            = delete;
      lock_guard& operator=(const lock_guard&) = delete;
      
    • 单一责任仅用于管理互斥锁中的mutex不支持其他复杂的锁定策略

2.4 unique_lock

  • lock_guard存在的不足

    • 缺乏灵活性无法自由的进行加锁和解锁,可能因为作用域过大而导致锁的粒度过大

      尽管可以使用大括号{}圈定作用范围

    • 不支持复杂的互斥锁

    • 不支持条件变量

    • 不支持尝试加锁

  • 构造器

    unique_lock(_Mutex& _Mtx)自动上锁

    unique_lock(_Mutex& _Mtx, adopt_lock_t)接管已经上锁的锁

    unique_lock(_Mutex& _Mtx, defer_lock_t)延迟上锁,不进行自动上锁(没有延迟操作)

    unique_lock(_Mutex& _Mtx, try_to_lock_t)尝试上锁

    unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time)指定时间内尝试加锁

    unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time)指定时刻前尝试加锁

  • 成员函数

    lock()

    unlock()unique_lock的解锁既可以手动解锁,也可以在其析构函数中解锁,既可以自由控制解锁,也防止了忘记解锁的误操作

    try_lock()

    try_lock_for()

    try_lock_until()

    owns_lock()返回一个bool类型的值,表示对象是否拥有锁的所有权(是否上锁),可以搭配构造器中的尝试上锁操作使用

  • 特点

    • 灵活性提供更加多样的控制选项
    • 支持手动控制,也具备自动控制可以手动解锁,也可以等待析构解锁
    • 可移动性并没有禁用析构函数和赋值运算符,可以进行拷贝,赋值和移动

2.5 读写锁

允许多个读线程并发读取资源,但写入资源时只允许一个线程也就是一个写线程独占访问共享资源

读读不互斥,读写互斥,写写互斥

  • shared_mutex类

    执行成员函数的不同,该锁的功能也不一样

    • 独占锁(写锁)

      • lock()

      • try_lock()

      • unlock()

    • 共享锁(读锁)

      • lock_shared()
      • try_lock_shared()
      • unlock_shared()

    虽然它有两套不同的成员函数来执行它不同的功能,但是通常不直接调用它的成员函数,而是分别使用其它不同的类来进行管理

  • shared_lock类

    类似于unique_lock

    shared_lock被称为通用共享互斥所有权包装器

    unique_lock被称为独占互斥所有权包装器

    shared_lock来实现shared_mutex共享功能

    unique_lock来实现shared_mutex独占功能

#include<iostream>
#include<thread>
#include<chrono>
#include<shared_mutex>
#include<vector>

std::shared_mutex rw_mutex;//创建读写锁对象
int shared_data = 0;

void Reader()
{
	int degree = 1000;
	while (degree--)
	{
		std::shared_lock<std::shared_mutex> lock(rw_mutex);//实现共享锁功能
		std::cout << "Reader thread " << std::this_thread::get_id() << " " << shared_data << std::endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}

void Writer()
{
	int degree = 1000;
	while (degree--)
	{
		std::unique_lock<std::shared_mutex> lock(rw_mutex);//实现独占锁功能
		std::cout << "Writer thread " << std::this_thread::get_id() << " " << ++shared_data << std::endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}

int main() {
	std::vector<std::thread> threads;
	
	for (int i = 0; i < 5; i++)
	{
		threads.push_back(std::thread(Reader));
	}

	for (int i = 0; i < 2; i++)
	{
		threads.push_back(std::thread(Writer));
	}

	for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
	{
		(*it).join();
	}
	return 0;
}

2.6 条件变量

  • 需要和包装互斥锁std::mutex的独占互斥所有权包装器std::unique_lock<std::mutex>配合使用

  • 并且需要在锁的作用范围内使用

条件变量是一种同步原语,在头文件<mutex>中,它能使一部分没有满足条件的线程等待(进入阻塞状态)条件满足,还能使一部分满足条件的线程去通知(唤醒)那一部分等待状态的线程

如果不去使用条件变量,如果我需要去协调两个线程,那么我需要在线程开始时设置一个条件并且使用while重复进行判断,这样CPU会浪费时间反复轮询这个条件,而条件变量的出现使得不满足条件的线程进入阻塞状态不去抢夺CPU的时间片以提高整个程序的效率,并且当阻塞条件满足时还可以通过唤醒功能唤醒被阻塞线程

  • 引入头文件,定义一个条件变量
#include<mutex>
std::condition_variable cond;
  • 成员函数

    • 阻塞功能

      • void wait(unique_lock<mutex>& _Lck)

        只能阻塞线程,需要额外的条件判断

      • void wait(unique_lock<mutex>& _Lck, _Predicate _Pred)

        因为可以传入谓词Predicate可以按照谓词提供的条件对线程进行区分来阻塞线程

      • cv_status wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time)

        一段时间内阻塞线程,返回一个cv_status枚举值

      • bool wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time, _Predicate _Pred)

        一段时间内阻塞线程,返回一个bool类型值

      • cv_status wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time)

        某时刻前阻塞线程,返回一个cv_status枚举值

      • bool wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time, _Predicate _Pred)

        某时刻前阻塞线程,返回一个bool类型值

    • 唤醒功能

      • void notify_one()用于唤醒一个正在等待该条件变量的线程。如果有多个线程在等待同一个条件变量,那么会选择其中一个线程进行唤醒

        具体哪个线程被唤醒是由操作系统调度策略决定的

      • void notify_all()用于唤醒所有阻塞的线程

  • 注意事项如果阻塞函数没有使用谓词参数,要达到预期条件则需要自己在函数外用while对条件进行重复判断

    • 💡 防止假唤醒
    • 如果使用if,当共享资源处于临界条件,唤醒的线程数量不唯一时会导致共享资源的越界访问
//一般使用方式1
std::unique_lock<std::mutex> lock(mutex);//加锁
while (判断条件)
{
	cond.wait(lock);
}
/*
访问共享资源
*/
cond.notify_all();//唤醒

//一般使用方式2
std::unique_lock<std::mutex> lock(mutex);//加锁
cond.wait(lock, 谓词判断函数);
/*
访问共享资源
*/
cond.notify_all();//唤醒
  1. 谓词_Predicate

predicate通常是指一个函数对象(function object),它可以是函数指针、lambda表达式、成员函数指针或实现了operator()的类的对象。谓词的主要用途是执行某种测试,返回一个布尔值(truefalse),以表示某个条件是否满足

  1. std::cv_status是一个枚举类
  • std::cv_status::no_timeout表示时间范围内谓词条件满足或者收到了唤醒信号

  • std::cv_status::timeout表示超时,即时间范围内谓词条件没有满足并且没有收到了唤醒信号

  1. 假唤醒

非预期的唤醒,是在多线程编程中使用条件变量时可能遇到的一种现象。当一个线程在等待某个条件变量时,它可能会被意外地唤醒,即使没有其他线程调用 notify_onenotify_all 来发送信号

可以使用谓词参数或者自己进行while的循环判断来预防

  • 线程在调用阻塞成员函数后发生了什么

    1. 释放锁

    2. 进入等待队列被阻塞,不再抢占CPU资源

    3. 等待条件或者通知

      • wait()

        只有当另一个线程调用同一个条件变量的唤醒函数,结束等待

      • cv_status wait_for() or wait_until不带谓词参数

        • 当另一个线程调用同一个条件变量的唤醒函数,结束等待
        • 假唤醒
        • 当超出时间范围,结束等待
      • bool wait_for() or wait_until带谓词参数

        • 当另一个线程调用同一个条件变量的唤醒函数,结束等待
        • 谓词函数返回true
        • 假唤醒
        • 当超出时间范围,结束等待
    4. 尝试获取锁

    5. **(检查条件)**预防假唤醒

      • 当函数带有谓词参数时,获取锁后会再次判断谓词参数条件是否成立

        • 不成立重新调用阻塞函数,进入等待队列

        • 成立则进行下一步

      • 如果不带有谓词参数则需要自行在阻塞函数外添加while循环判断条件

    6. 继续执行后续操作

📖实现的生产者消费者模型

void Producter()
{
	while (true)
	{
		std::unique_lock<std::mutex> lock(mutex);
        
		while (goods == 10)
		{
			cond.wait(lock);
		}
        
		//cond.wait(lock, []() {return goods != 10; });//上下两种写法二选一

		std::cout << "Productor thread " << std::this_thread::get_id() << " " << ++goods << std::endl;
		cond.notify_all();
	}

}

void Consumer()
{
	while (true)
	{
		std::unique_lock<std::mutex> lock(mutex);
        
		while (goods == 0)
		{
			cond.wait(lock);
		}

		//cond.wait(lock, []() {return goods != 0; });//上下两种写法二选一

		std::cout << "Consumer thread " << std::this_thread::get_id() << " " << --goods << std::endl;
		cond.notify_all();
	}
}

int main() {

	std::vector<std::thread> threads;
	
	for (int i = 0; i < 5; i++)
	{
		threads.push_back(std::thread(Producter));
	}

	for (int i = 0; i < 2; i++)
	{
		threads.push_back(std::thread(Consumer));
	}

	for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
	{
		(*it).join();
	}

	return 0;
}

2.7 信号量C++20

💡 信号量头文件<semaphore>只能在C++20版本中使用

信号量类似于条件变量,条件变量通过条件限制来控制线程访问共享变量,而信号量则使用自己自带的计数器通过计数器的加减和数量大小来限制通过的线程数量,从而实现对共享资源的保护

一般也需要搭配互斥锁使用,互斥锁来实现线程同步,信号量来保护共享资源

(也可以通过一个初始值为1的二元信号量Binary Semaphore来实现互斥锁功能,即一个计数器最大为1的信号量)

  • 初始化

    _Least_max_value该信号量计数器最大值应该大于等于0

    _Desired该信号量计数器初始值应该大于等于0

#include<semaphore>//包含头文件
std::counting_semaphore<_Least_max_value> productor_semaphore(_Desired);
  • 成员函数

    提供两个原子操作:P操作V操作

    • void acquire()P操作进行调用该操作的信号量的计数器的-1
    • void release(ptrdiff_t _Update = 1)V操作进行调用该操作的信号量的计数器的+1,默认一次只+1
  • 注意事项

    • 不可复制和移动拷贝和赋值运算符被禁用
    • 如果和互斥锁配合使用,一定将P操作写在加锁操作前,否则如果当某个信号量计数器达到临界值,又有线程抢到时间片继续访问该信号量,进行加锁后因为计数器达到临界值被阻塞,其他线程拿不到互斥锁也被阻塞,导致死锁

📖实现的生产者消费者模型

使用初始值为1的二元信号量实现的互斥锁的线程同步

#include<semaphore>

std::mutex mutex;
std::counting_semaphore<1> synchronous_semaphore(1);//实现线程同步的信号量
//开始没有东西进行消费,位置一共10个
std::counting_semaphore<10> productor_semaphore(10);//生产者的信号量
std::counting_semaphore<10> consumer_semaphore(0);//消费者的信号量

void Producter()
{
	while (true)
	{
		productor_semaphore.acquire();//生产者生产一个产品

		//std::unique_lock<std::mutex> lock(mutex);//也可以使用互斥锁
		synchronous_semaphore.acquire();//模拟上锁

		std::cout << "Productor thread " << std::this_thread::get_id() << " " << ++goods << std::endl;

		synchronous_semaphore.release();//模拟解锁

		consumer_semaphore.release();//告诉消费者多一个产品可以消费
	}

}

void Consumer()
{
	while (true)
	{
		consumer_semaphore.acquire();//消费者消费一个产品

		//std::unique_lock<std::mutex> lock(mutex);//也可以使用互斥锁
		synchronous_semaphore.acquire();//模拟上锁

		std::cout << "Consumer thread " << std::this_thread::get_id() << " " << --goods << std::endl;
		
		synchronous_semaphore.release();//模拟解锁

		productor_semaphore.release();//告诉生产者多一个产品可以生产
	}
}

int main() {

	std::vector<std::thread> threads;
	
	for (int i = 0; i < 5; i++)
	{
		threads.push_back(std::thread(Producter));
	}

	for (int i = 0; i < 2; i++)
	{
		threads.push_back(std::thread(Consumer));
	}

	for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
	{
		(*it).join();
	}

	return 0;
}

2.8 原子操作

原子操作是指不可中断的操作,即该操作在执行过程中不会被线程调度机制打断,因此也不会出现数据的不一致性,从而可以不需要额外的同步机制如互斥锁

  • 原子变量

    • 在C++11标准库中提供了std::atomic模板类来支持原子操作,可以使用该模版类创建一个原子变量,其中提供的成员函数确保了所有对其成员变量的操作都是原子的

    • 主要特性

      • 原子性
      • 易用性提供了一组简洁易于使用的接口
      • 无锁原子操作可以不需使用锁机制,因此不会引起上下文切换,具有更高的性能
      • 内存序Memory Ordering原子变量允许指定内存序,以控制操作的顺序和可见性,用以避免内存序问题的产生
    • 需要注意的问题

      • 内存序问题需要通过使用C++标准定义的内存序规则来规避该问题
      • ABA问题错误使用比较并交换CAS(Compare And Swap)
      • 复合操作的原子性单个原子操作是原子的,但是多个原子操作组合成的复合操作并不自动保持原子性
      • 平台依赖性不同的处理器架构可能支持不同的原子操作。虽然C++标准库提供的原子操作是跨平台的,但某些特定的原子操作可能在某些平台上不可用或性能较差
      • 性能考虑在高竞争的情况下,原子操作可能不如细粒度的锁效率高
      • 调试难度原子操作不像锁那样留下明显同步点
      • 数据大小限制不是所有的数据类型和大小都支持原子操作

为什么会有内存序问题?

  • 编译器优化编译器在编译代码时,可能会改变代码中的操作顺序,以生成更加高效的机器代码
  • 处理器优化处理器在执行命令时,可能会改变指令执行的顺序,以提高执行效率
  • 缓存一致性处理器缓存中的数据可能与主内存中的数据不同步,不同处理器核心对同一内存位置的读取结果可能不同

如何解决?

  • 内存序规则C++11引入了新的内存模型,定义了一组内存序规则,这些规则通过std::memory_order枚举来指定
  • std::atomic模板类的成员函数提供了相应的内存序选项,可以在操作中指定具体内存序规则

内存序规则

  • std::memory_order_relaxed:仅保证原子性,不提供同步语义
  • std::memory_order_consume:类似于 memory_order_acquire,只对通过指针或引用传递的数据产生同步效果,少见很难正常使用
  • std::memory_order_acquire:用于读操作,确保后续读写操作不会被重排到此操作之前
  • std::memory_order_release:用于写操作,确保先前读写操作不会被重排到此操作之后
  • std::memory_order_acq_rel:结合了 memory_order_acquirememory_order_release 的特性
  • std::memory_order_seq_cst:提供最强的同步保证,确保全局顺序一致性

(读操作使用std::memory_order_acquire,写操作使用std::memory_order_release,读写操作都有使用std::memory_order_acq_rel

  • 创建一个原子变量
std::atomic<参数类型> 参数名称(初始化的值);
  • 原子成员函数

    • bool is_lock_free()

      检查该原子操作是否是无锁的

      • 返回值无锁为1有锁为0
    • _Ty load(const memory_order _Order)

      原子的读取并且返回当前值

      • 参数_Order内存顺数(选填)
      • 返回值返回该对象的值
    • void store(const _Ty _Value, const memory_order _Order)

      原子的将_Value值存储到对象中

    • _Ty exchange(const _Ty _Value, const memory_order _Order)

      原子的将_Value值交换到对象中,并返回对象的原来值

      • 返回值该对象原来的值
    • bool compare_exchange_weak(_Ty& _Expected, const _Ty _Desired, const memory_order _Success,const memory_order _Failure)

      CAS比较交换

      当对象值和_Expected值相等时,对象值被替换为_Desired并返回true

      当对象值不相等时,将_Expected值更新为对象值,并且返回false

      weak设计上是允许失效的伪失败,可能会因为内存竞争或其他原因在某些实现中返回 false,并且可能会更频繁地失败,但是更高效

      • 参数
        • _Expected需要时一个和该对象同数据类型的参数
        • _Success and _Failure内存顺数(选填)
      • 返回值是否交换成功
    • bool compare_exchange_strong(_Ty& _Expected, const _Ty _Desired, const memory_order _Success,const memory_order _Failure)

      CAS和上述功能一致

      strong保证只有在原子变量的值确实不等于预期值时才会返回false。如果原子变量的值等于预期值,那么它一定会成功更新原子变量的值,并返回true

    (只支持部分数据类型)

    • _Ty fetch_add(const _Ty _Operand, const memory_order _Order)

      _Ty fetch_sub(const _Ty _Operand, const memory_order _Order)

      原子的增加或者减少_Operand,并且返回操作之前的值

    • _Ty fetch_and(const _Ty _Operand, const memory_order _Order)

      _Ty fetch_or(const _Ty _Operand, const memory_order _Order)

      _Ty fetch_xor(const _Ty _Operand, const memory_order _Order)

      原子的将对象值与_Operand执行按位与、按位或、按位异或操作,并返回操作前的值

  • 原子操作符

    • 赋值操作符(=

      例如:atomic_value = value;

    • 取值操作符(T()

      例如:T value(atomic_value); 或者 T value = atomic_value;

    • 前置后置递增递减操作符(++a, a--

      例如:++atomic_value 或者 atomic_value--

    • 复合赋值操作符(+=, -=, &=, |=, ^=

      例如:atomic_value += value;

📖实现的生产者消费者模型

使用原子变量实现一个5个生产者和2个消费者资源最大为10最小为0的生产者消费者模型,不使用互斥锁

//不使用内存序参数版本
#include<iostream>
#include<vector>
#include<thread>
#include<chrono>

std::atomic<int> atomic_int(0);//共享资源,初始化为0
std::atomic<bool> atomic_bool(true);//用于终止整个程序

void producer()
{
    while (atomic_bool.load())//终止条件
    {
        //选择使用,使不满足条件的线程让出时间片提高程序效率
        //可以选择使用是因为current < 10的存在
        while (atomic_int.load() >= 10)
        {
            std::this_thread::yield();
        }

        int current = atomic_int.load();
        //current < 10:避免在current初始化前atomic_int被其他线程增加为10导致越界访问
        if (current < 10 && atomic_int.compare_exchange_weak(current, current + 1))
        {
            //虽然警告但是输出规整
            printf("Producer thread %lu : %d\n", std::this_thread::get_id(), current + 1);
        }
    }
}

void consumer()
{
    while (atomic_bool.load())//终止条件
    {
        //选择使用,使不满足条件的线程让出时间片提高程序效率
        //可以选择使用是因为current > 0的存在
        while (atomic_int.load() <= 0)
        {
            std::this_thread::yield();
        }

        int current = atomic_int.load();
        //current > 0:避免在current初始化前atomic_int被其他线程减少为0导致越界访问
        if (current > 0 && atomic_int.compare_exchange_weak(current, current - 1))
        {
            //虽然警告但是输出规整
            printf("Consumed thread %lu : %d\n", std::this_thread::get_id(), current - 1);
        }
    }
}

int main() {
    std::vector<std::thread> threads;

    for (int i = 0; i < 5; i++)//创建5个消费者线程
    {
        threads.emplace_back(producer);
    }

    for (int i = 0; i < 2; i++)//创建2个消费者线程
    {
        threads.emplace_back(consumer);
    }

    std::this_thread::sleep_for(std::chrono::seconds(5));//运行5s
    atomic_bool.store(false);//使所有线程终止

    for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
    {
        (*it).join();//阻塞回收
    }

    std::cout << "Final count: " << atomic_int.load() << std::endl;
    return 0;
}

//使用内存序参数版本
//读操作使用std::memory_order_acquire,写操作使用std::memory_order_release,读写操作都有(比如说CAS)使用std::memory_order_acq_rel
#include<iostream>
#include<vector>
#include<thread>
#include<chrono>

std::atomic<int> atomic_int(0);
std::atomic<bool> atomic_bool(true);

void producer()
{
    while (atomic_bool.load(std::memory_order_acquire))
    {
        while (atomic_int.load(std::memory_order_acquire) >= 10)
        {
            std::this_thread::yield();
        }

        int current = atomic_int.load(std::memory_order_acquire);

        if (current < 10 && atomic_int.compare_exchange_weak(current, current + 1, std::memory_order_acq_rel))
        {
            printf("Producer thread %lu : %d\n", std::this_thread::get_id(), current + 1);
        }
    }
}

void consumer()
{
    while (atomic_bool.load(std::memory_order_acquire))
    {
        while (atomic_int.load(std::memory_order_acquire) <= 0)
        {
            std::this_thread::yield();
        }

        int current = atomic_int.load(std::memory_order_acquire);
        if (current > 0 && atomic_int.compare_exchange_weak(current, current - 1, std::memory_order_acq_rel))
        {
            printf("Consumed thread %lu : %d\n", std::this_thread::get_id(), current - 1);
        }
    }
}

int main() {
    std::vector<std::thread> threads;

    for (int i = 0; i < 5; i++)
    {
        threads.emplace_back(producer);
    }

    for (int i = 0; i < 2; i++)
    {
        threads.emplace_back(consumer);
    }

    std::this_thread::sleep_for(std::chrono::seconds(5));
    atomic_bool.store(false, std::memory_order_release);

    for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
    {
        (*it).join();
    }

    std::cout << "Final count: " << atomic_int.load() << std::endl;
    return 0;
}
  1. 生产者和消费者的打印顺序混乱是正常的使用cout打印断断续续也是正常的

因为如果只使用原子操作,线程不会像使用锁那样被阻塞一个一个排队对共享资源进行访问,多个线程可以同时对共享资源进行访问(又因为共享资源是原子变量,访问共享资源的操作又是原子操作,也不会出现数据竞争问题),因此可能出现多个线程争抢打印函数的情况,这才导致了打印顺序混乱和断断续续

  1. 为什么消费和生产操作不直接使用原子操作自减或者自增而是使用CAScompare_exchange呢?

因为共享资源的总量是有限的,需要对当前的资源数进行判断再进行改变,又因为多个线程可以同时对共享资源进行访问(如1中所述),所以单纯的分开进行判断后进行数据的改变是非原子性的

  • (如果当前共享资源还差1就满了,此时一个生产者线程通过判断条件,但是该线程在进行生产最后一个共享资源操作前时间片结束了,被另一个判断条件外的生产者线程抢到了下一个时间片,由于上一个线程并没有成功对最后一个共享资源进行增加,所以第二个生产者线程也通过了判断条件,两个生产者但是只有最后一个共享资源生产空间,这就导致了访问越界)

因此要保证判断和对共享资源的改变(读操作和写操作)这两个操作一定要是原子的,CAScompare_exchange便可以实现这个功能

  • 我们需要一个参数current对访问CAS之前的共享资源数目进行记录,当进行CAS时atomic_int.compare_exchange_weak(current, current + 1 or current - 1)current和进行CAS时的共享资源进行比较,如果相等则可以认为记录current后和进行CAS前之间这段时间内共享资源的数量是没有被修改的(当然也有可能生产数量等于消费数量),不相等则重新进入生产函数。又因为CAS是原子操作,如果上述相等成立便可以保证记录current操作和CAS操作这两个操作之间的操作是原子性的。
  1. 为什么还要对current进行判断?不会影响原子性么?

因为2中仅仅只能保证记录current操作和CAS操作这两个操作之间的操作是原子性的,并没有考虑到在记录前共享资源前共享资源就已经达到临界值的情况。当一个线程执行到在判断条件while (atomic_int.load(std::memory_order_acquire) >= 10)后,在进行记录current操作前时,共享资源的值被其他同类线程改变到临界值,由于数值到达临界值其他同类线程通不过判断条件,如果共享资源不被另一类线程修改的话该线程可以顺利进行后续的操作,导致访问越界。因此为了解决这种情况,需要在进行CAS前对current进行临界值的判断。

  • 与此同时会发现,增加这一个操作后,判断条件while (atomic_int.load(std::memory_order_acquire) >= 10)可以省略了

current并不是原子变量,这些条件检查是在局部变量 current 上进行的,而不是直接在原子变量 count 上进行,并不会破坏 count 的原子性。

2.9 栅栏C++20

💡 信号量头文件<barrier>只能在C++20版本中使用

同步原语,它能阻塞一定量的线程在同一位置,当阻塞线程的数量到达指定数目,解除所有被阻塞的线程,继续执行

  • 创建一个栅栏对象
barrier(const ptrdiff_t _Expected, _Completion_function _Fn = _Completion_function()) {}

//可以直接忽略这个_Completion_function参数
std::barrier<> bar(想要阻拦线程的数量);
std::barrier bar(想要阻拦线程的数量);

_Completion_function a function object type,must meet the requirements of MoveConstructible and Destructible. std::is_nothrow_invocable_v<CompletionFunction&> must be true.

_Completion_function是一个函数模板,而且必须满足可移动构造可析构以及不会抛出异常

//你可以这样使用这个函数模板
const int num_threads = 10;

auto function = []() noexcept
 {
     std::cout << "finished" << std::endl;//每当栅栏预期值减为0时运行该函数
 };

std::barrier bar(num_threads, function);//注意这里的barrier不能加<>
  • 成员函数

    • arrive()

      当线程到达栅栏,使预期值-1

    • wait()

      当线程到达栅栏,阻塞该线程,阻塞直到该栅栏预期值减为0

    • arrive_and_wait()

      当线程到达栅栏,使预期值-1并且阻塞该线程,阻塞直到该栅栏预期值减为0

    • arrive_and_drop()

      当线程到达栅栏,使预期值-1并且当预期值为0时该栅栏将被永久释放

  • 注意事项

    • 在局部作用域内创建栅栏

      通常建议在局部作用域内创建 std::barrier,以确保其生命周期是受控的,这样可以避免在屏障不再需要时仍然占用资源,或者在屏障尚未释放时就已经被销毁,当函数退出时,局部变量会自动销毁,这样可以确保 std::barrier 在不再需要时被正确释放

  • 应用

    当work1和work2分别的5名员工都准备完成时,再去进行各自的工作

#include<iostream>
#include<vector>
#include<thread>
#include<barrier>

void work1_thread(const int& id, std::barrier<>& b)
{
    printf("work1 thread %d ready.\n", id);
    b.arrive_and_wait();
    printf("work1 thread %d begin to do.\n", id);
}

void work2_thread(const int& id, std::barrier<>& b)
{
    printf("work2 thread %d ready.\n", id);
    b.arrive_and_wait();
    printf("work2 thread %d begin to do.\n", id);
}

int main() {
    const int num_threads = 10;
    std::barrier<> bar(num_threads);//创建栅栏

    std::vector<std::thread> threads;

    for (int i = 0; i < 5; i++)
    {
        threads.emplace_back(work1_thread, i, std::ref(bar));
    }

    for (int i = 0; i < 5; i++)
    {
        threads.emplace_back(work2_thread, i, std::ref(bar));
    }

    for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
    {
        (*it).join();
    }

    printf("works has been done.\n");
    return 0;
}

//使用函数模版
#include<iostream>
#include<vector>
#include<thread>
#include<barrier>

auto function = []() noexcept//传入的函数模板
    {
        std::cout << "finished" << std::endl;
    };
 
void work1_thread(const int& id, std::barrier<decltype(function)> & b)
{
    printf("work1 thread %d ready.\n", id);
    b.arrive_and_wait();
    printf("work1 thread %d begin to do.\n", id);
}

void work2_thread(const int& id, std::barrier<decltype(function)> & b)
{
    printf("work2 thread %d ready.\n", id);
    b.arrive_and_wait();
    printf("work2 thread %d begin to do.\n", id);
}

int main() {
    const int num_threads = 10;

    std::barrier<decltype(function)> bar(num_threads, function);

    std::vector<std::thread> threads;

    for (int i = 0; i < 5; i++)
    {
        threads.emplace_back(work1_thread, i, std::ref(bar));
    }

    for (int i = 0; i < 5; i++)
    {
        threads.emplace_back(work2_thread, i, std::ref(bar));
    }

    for (std::vector<std::thread>::iterator it = threads.begin(); it != threads.end(); it++)
    {
        (*it).join();
    }

    printf("works has been done.\n");
    return 0;
}

decltype是C++11引入的一个关键字,用于获取表达式类型,可以用于声明变量,返回值类型,模版参数等场景

int a = 5;
decltype(a) b = 10; // b 的类型是 int

2.10 死锁

  • 是什么

    死锁是一种计算机科学中的状态,发生在操作系统中多个线程或者进程因争夺有限资源而进入无限期等待的状态,每个线程或者进程都在等待其他进程释放自己所需的资源。在这种情况下,除非采取外部措施,否则所有涉及的进程都无法继续执行

  • 如何产生

    多个线程或者进程互相等待对方释放资源

    产生死锁必须同时满足以下四个必要条件(也称为Coffman条件):

    • 互斥条件 (Mutual Exclusion):至少有一种资源是不可共享的,如果另一个线程或者进程请求该资源,则必须等到资源被释放

    • 占有并等待条件 (Hold and Wait):存在一个线程或者进程已经持有了至少一个资源,但又提出了新的资源请求,而在新的请求未得到满足的情况下,该进程不会释放它已经持有的资源

    • 非抢占条件 (No Preemption):其他线程或者进程不能强行剥夺已经分配给一个线程或者进程的资源,资源只能由持有者自愿释放

    • 循环等待条件 (Circular Wait):存在一组等待进程{P1, P2, …, PN},其中P1正在等待P2所持有的资源,P2正在等待P3所持有的资源,以此类推,直到PN正在等待P1所持有的资源,形成一个循环链

3 异步编程

异步和多线程

  • 异步和多线程的关系

    • 异步是最终目标

    • 多线程是实现异步的一种方式

      但是对于I/O密集型任务,多线程可能不是最理想的,理由如下

      • 创建和销毁线程有较高的开销
      • 线程间需要进行同步以避免竞态条件(race conditions)和死锁(deadlocks),这可能会增加编程复杂度
      • 大量线程可能导致上下文切换频繁,影响性能
  • 异步Asynchronous

    • 定义:异步是一种编程模式,其中函数调用不会立刻返回结果,被调用的函数在后台处理其任务而不会阻塞当前线程或者进程。一旦任务完成或者达到某个预定的状态,会通过某些机制来通知调用者
    • 适用场景
      • I/O密集型任务,如网络请求、磁盘读写等
      • 需要保持用户界面响应的情况下
      • 任何可以被中断并稍后恢复的任务
  • 多线程Multithreading

    • 定义:是一种并发编程技术,它允许一个程序同时执行多个线程,每个线程都可以独立执行任务,共享进程的资源
    • 适用场景
      • CPU密集型任务,如复杂的计算、图像处理等
      • 需要在同一个程序中同时执行多个任务,并且这些任务可能需要访问共享资源

3.1 std::future

C++标准库中提供的一个模板类,谓语<future>头文件内,std::future主要用于获取异步操作的结果,这些异步操作通常是由std::asynstd::package_task或者std::promise创建的

std::future对象提供了一种安全的方式去访问异步任务的结果,只有当结果准备好后,才能通过该对象访问它

  • 成员函数

    • _Ty& get()

      获取异步操作的结果如果操作尚未完成,调用该函数的线程将被阻塞,直到操作完成

      💡 一旦调用,该std::future对象将不再与任何共享状态相关联(即变为无效状态,如果继续使用该对象将抛出异常)

    • void wait()

      阻塞该线程等待异步操作完成,不获取结果

    • future_status wait_for(const chrono::duration<_Rep, _Per>& _Rel_time)

      future_status wait_until(const chrono::time_point<_Clock, _Dur>& _Abs_time)

      在指定时间范围内等待异步操作完成

      • 返回值
        • 已经完成返回std::future::ready
        • 还未完成返回std::future::timeout
        • 尚未启动返回std::future::deferred
    • bool valid()

      检查std::future对象是否与有效的共享状态相关联

      • 返回值
        • 关联返回1
        • 不关联返回0

3.2 std::async

模版函数,位于future头文件中

  • 概述

    用于启动一个异步任务

    • 可以选择在新的一个线程中运行

    • 也可以选择在调用get()或者wait()时在当前线程中运行

  • 模版函数声明

    future<_Invoke_result_t<decay_t<_Fty>, decay_t<_ArgTypes>...>> async(launch _Policy, _Fty&& _Fnarg, _ArgTypes&&... _Args) {

    • 参数
      • _Policy启动策略
        • std::launch::async表示异步运行(启动一个新线程)
        • std::launch::deferred表示延迟运行(直到 get()wait() 被调用,在当前线程运行)
        • std::launch::async | std::launch::deferred**(default)**表示选择 async 还是 deferred 的决定权在于实现,标准库可以根据系统资源情况或者其他因素自行决定何时以及如何执行任务
      • _Fnarg要异步执行的函数或者lambda表达式
      • _Args传递给 func 的参数列表
    • 返回值类型
      • std::future对象,用于获取异步操作结果
  • 例子

#include<iostream>
#include<thread>
#include<future>
#include<chrono>

int function_add(int a, int b)
{
	std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
	std::this_thread::sleep_for(std::chrono::seconds(2));
	return a + b;
}

int main()
{
	std::future<int> result = std::async(std::launch::async, function_add, 10, 20);//异步运行

	std::this_thread::sleep_for(std::chrono::seconds(1));

	std::cout << "This main thread " << std::this_thread::get_id() << std::endl;

	std::cout << "The result is " << result.get() << std::endl;

	result = std::async(std::launch::deferred, function_add, 10, 30);//延时运行

	std::cout << "This main thread " << std::this_thread::get_id() << std::endl;

	result.wait();

	std::cout << "The result is " << result.get() << std::endl;

	std::cout << "Next work... " << std::endl;

	return 0;
}

3.3 std::promise

模板类,位于<future>头文件中,它与std::future以及std::thread一起使用来实现线程之间的通信,需要创建一个线程并且把std::future对象作为参数传入

std::promise对象用来储存一个值,而std::future对象则来获取这个值,这使得一个线程可以在某个时间点设置数据而另一个线程可以在稍后的某个时间点读取这个数据,从而实现数据交换

std::async一样可以获取异步操作中的数据,但是std::async需要等待异步操作结束返回值才能接收到这个数据,而std::promise可以在异步操作执行过程中的任意时间将数据接收

  • 创建对象
std::promise<期望接收数据的数据类型> promise_;
  • 成员函数

    • future<_Ty&> get_future()

      返回一个带有同样模板的std::future对象,并与其建立共享关联状态

      💡 get_future 只能调用一次;多次调用会导致未定义行为

    • void set_value(_Ty& _Val)

      void set_exception(exception_ptr _Exc)

      设置一个std::promise的值或者是一个异常,这时调用std::futureget()便可以返回该数据

    • void set_value_at_thread_exit(_Ty& _Val)

      void set_exception_at_thread_exit(exception_ptr _Exc)

      设置值的操作会在创建 std::promise线程退出时发生

  • 注意事项

    • 移动而非复制std::promise不支持拷贝构造和赋值操作符,但支持移动语义,因此应该使用std::move对其所有权进行转移

      💡但是一旦std::promise被移动,原始对象就将进入无效状态

      • ‘失效’或者说‘不能再被安全的使用’

        • 设置值或者异常后

          一旦设置了值或异常,std::promise 就不再处于可设置状态

        • 析构

          std::promise 对象被销毁后,它就无法再被使用。如果 std::promise 在设置值或异常之前就被销毁,而对应的 std::future 仍在等待,则 std::future 将永远不会被满足,导致死锁或其他未定义行为

        • 移动语义

          std::promise 被移动std::move到另一个 std::promise 后,**原来的 **std::promise 将进入一个无效状态,不能再被用来设置值或异常

  • 左值和右值

    • 左值lvalue(即有名称的对象,永久存在的对象)
    • 右值rvalue(即不能有名称的对象,临时对象)
  • 如何区分?

    • 凡是取地址(&)操作可以成功的都是左值,其余都是右值
  • 引用(reference)

    都可以避免不必要的拷贝,从而提升程序的效率

    • 左值引用(lvalue reference) (T &)

      是对左值的引用

    • 右值引用(Rvalue Reference) (T &&)

      是对右值的引用,主要用于支持移动语义完美转发

  • 移动语义move semantic

    是一种优化技术,它允许资源从一个对象“移动”到另一个对象,而不是进行深拷贝这通常用于临时对象或即将被销毁的对象,以提高性能

    当你对一个左值使用右值引用(移动语义)后,原始对象通常是状态通常是未定义的(除非该类的移动构造函数或移动赋值运算符明确地设置了某种有效状态),因此,虽然技术上你可能仍然可以访问原始对象,但这通常不是一个好主意,因为你无法预测其状态

    • std::move()

      remove_reference_t<_Ty>&& move(_Ty&& _Arg)自动推导模版类型,因为返回值只能是一个右值引用,可以通过不需要显式的模板参数列表

      用于实现移动语义,无条件将一个参数转换为右值

      将一个对象标记为可以被移动(move)的状态,而不是被复制(copy),使得可以调用对象的移动构造函数或移动赋值运算符,从而实现资源的高效转移

  • 完美转发perfect forward

    是一种通过通用引用实现的技术,它允许你在传递参数时保留其原始的值类别(左值或右值),这对于编写泛型代码特别有用,因为它允许你将参数原封不动地传递给另一个函数

    • 通用引用universal reference

      用来特指一种引用的类型,构成通用引用有两个条件:

      1. 必须满足T &&这种形式

      2. 类型T必须是通过推断得到的(右值引用的T可以是已经确定好的)

        例如函数模版参数,atuo声明…

      可以通过引用类型合成(Reference Collapsing Rules),实现传进来的是左值引用就是左值引用,右值引用就是右值引用

    • std::forward<_Ty>()

      _Ty&& forward(remove_reference_t<_Ty>& _Arg) 需要显式模板参数,因为它的目的是保留原始参数的值类别,模板参数 _Ty 决定了 _Ty&& 是左值引用还是右值引用

      它主要用于实现完美转发,保留参数的左右值类型

  • 例子

    摘自:移动语义(move semantic)和完美转发(perfect forward)-优快云博客

class Test {
    int * arr{nullptr};
public:
    Test():arr(new int[5000]{1,2,3,4}) { 
    	cout << "default constructor" << endl;
    }
    Test(const Test & t) {
        cout << "copy constructor" << endl;
        if (arr == nullptr) arr = new int[5000];
        memcpy(arr, t.arr, 5000*sizeof(int));
    }
    Test(Test && t): arr(t.arr) {//移动语义
        cout << "move constructor" << endl;
        t.arr = nullptr;//注意虽然是浅拷贝,但是将原来arr的指针置空,防止重复析构
    }
    ~Test(){
        cout << "destructor" << endl;
        delete [] arr;
    }
};

Test createTest() {
    return Test();//先构造后移动语义
}

//在main当中调用relay,Test的临时对象作为一个右值传入relay,在relay当中又被转发给了func,那这时候转发给func的参数t也应当是一个右值,左值同理
template <typename T>
void func(T t) {
    cout << "in func " << endl;
}

template <typename T>
void relay(T&& t) {//实现通用引用
    cout << "in relay " << endl;
    func(std::forward<T>(t));//使用forward保持参数的左右值类型
}

int main() {
	Test reusable;
    //实现移动语义
    Test t(createTest());//由于强制的(N)RVO,可能不会执行移动语义

    Test duplicated(std::move(reusable));//使用std::move显式地进行移动
    //执行后reusable中的arr被置为悬挂指针,不能继续使用

    //实现完美转发
    relay(Test());//右值

    relay(reusable);//左值
    //函数中并不涉及arr的使用,可以调用
    return 0;
}
  • 例子
#include<iostream>
#include<thread>
#include<future>
#include<chrono>

void funtion(std::promise<int> && promise_)//右值引用
{
	std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
	std::this_thread::sleep_for(std::chrono::seconds(1));
    
	int value = 100;
	promise_.set_value(value);//设置值,可以立即被获取
    
	std::this_thread::sleep_for(std::chrono::seconds(1));
	std::cout << "Next work... " << std::endl;
}

int main()
{
	std::promise<int> promise_;//创建对象
	std::future<int> future_ = promise_.get_future();//从promise对象获取一个future对象
	std::thread my_thread(funtion, std::move(promise_));//转换为右值作为参数创建一个线程
    
	std::cout << "This main thread " << std::this_thread::get_id() << " promise " << future_.get() << std::endl;//获取异步操作中的数据
    
	my_thread.join();
	return 0;
}

3.4 std::packaged_task

C++标准库中的模版类,位于<future>头文件中

  • 概述

    用于封装一个可调用对象(如函数、lambda表达式、函数对象等)及其相关状态,以便于异步的执行这个任务,并且可以std::future对象来获取结果,也可以通std::thread启动这个异步操作,也可以直接像函数一样直接在当前线程调用

  • 创建对象

    packaged_task(_Fty2&& _Fnarg)

    传入一个可调用对象(如函数、lambda表达式、函数对象等)

  • 成员函数

    • future<_Ret> get_future()

      返回一个带有同样模板的std::future对象,并与其建立共享关联状态

      💡 get_future 只能调用一次;多次调用会导致未定义行为

    • swap(packaged_task& _Other)

      交换两个 std::packaged_task 对象的内容。这对于避免不必要的复制或移动操作很有用

    • bool valid()

      检查 packaged_task 是否有效,如果 packaged_task 已经被移动或已经执行过,则无效

  • 例子

#include<iostream>
#include<thread>
#include<future>
#include<chrono>

int task1(int a, int b)
{
	std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
	return a + b;
}

int main()
{
	std::packaged_task<decltype(task1)> ptask1(task1);//函数创建对象

	std::packaged_task<int(int, int)> ptask2([](int a, int b) -> int {
		std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
		return a + b;
		});//lambda表达式创建对象

	std::future<int> future1 = ptask1.get_future();//给出对应std::future对象
	std::future<int> future2 = ptask2.get_future();//给出对应std::future对象

	std::cout << "Ptask1 valid " << ptask1.valid() << std::endl;
	std::cout << "Ptask2 valid " << ptask2.valid() << std::endl;

	std::thread my_thread1(std::move(ptask1), 10, 10);//使用std::thread启动异步操作
	std::thread my_thread2(std::move(ptask2), 20, 20);//使用std::thread启动异步操作

    //ptask1(10, 10);//直接像函数一样调用
    //ptask2(20, 20);
    
	std::cout << "moved Ptask1 valid " << ptask1.valid() << std::endl;
	std::cout << "moved Ptask2 valid " << ptask2.valid() << std::endl;

	std::cout << "This main thread " << std::this_thread::get_id() << " my_thread1 " << future1.get() << std::endl;
	std::cout << "This main thread " << std::this_thread::get_id() << " my_thread2 " << future2.get() << std::endl;

	my_thread1.join();
	my_thread2.join();

	return 0;
}

3.5 比较

  • std::async

    • 概述

      std::async 用于异步执行函数,并提供一个 std::future 来获取结果

    • 特点

      • 可封装可调用对象
      • 简单易用,可以自动启动异步任务,还可以可以选择同步或者异步策略
      • 灵活性有限,无法自由控制任务启动以及结果传递
    • 适用场景

      简单的异步任务

  • std::promise

    • 概述

      std::promise 用于存储一个值,该值稍后可以由 std::future 获取

    • 特点

      • 可以手动设置 std::future 的值
      • 灵活性,手动控制任务启动以及结果传递
    • 适用场景

      需要手动控制任务结果的传递,线程间的通信

  • std::packaged_task

    • 概述

      std::packaged_task 封装了一个可调用对象(如函数或 lambda),并提供一个 std::future 来获取执行结果

    • 特点

      • 可封装可调用对象
      • 灵活,手动控制任务启动以及结果传递,易于与其他工具结合使用
    • 适用场景

      高灵活的任务管理

  • 例子

    std::asyncstd::packaged_task分别使用一个std::promise进行数据的接收,输出std::asyncstd::packaged_task的返回值以及std::promise分别在这两个线程中接收的值

#include<iostream>
#include<thread>
#include<future>

int task1(std::promise<int> && promise, int a, int b)
{
	std::cout << "This future_async child thread " << std::this_thread::get_id() << std::endl;
	
	promise.set_value(111);

	return a + b;
}

int main()
{
	std::promise<int> promise1;
	std::promise<int> promise2;

	std::future<int> future1 = promise1.get_future();
	std::future<int> future2 = promise2.get_future();

	std::future<int> future_async = std::async(std::launch::async, task1, std::move(promise1), 10, 10);

	std::packaged_task<int(std::promise<int> && , int, int)> ptask([](std::promise<int> && promise, int a, int b) -> int {
		std::cout << "This packaged_task child thread " << std::this_thread::get_id() << std::endl;
		promise.set_value(222);
		return a + b;
		});

	std::future<int> future_packaged_task = ptask.get_future();

	std::thread my_thread(std::move(ptask), std::move(promise2), 20, 20);

	std::cout << "This future_async main thread " << std::this_thread::get_id() << " future_async " << future_async.get() << std::endl;
	std::cout << "future_async promise " << future1.get() << std::endl;

	std::cout << "This future_packaged_task main thread " << std::this_thread::get_id() << " future_packaged_task " << future_packaged_task.get() << std::endl;
	std::cout << "future_packaged_task promise " << future2.get() << std::endl;

	my_thread.join();

	return 0;
}

4 线程池

线程池是一种预先创建一组线程的机制,这些线程创建好后,等待任务分配,当有任务需要执行时,线程池会从线程集合中分配线程来执行任务,而不是每次都重新创建和销毁线程

在这里插入图片描述

  • 为什么要使用线程池?

    • 提高性能避免了频繁的创建和销毁线程
    • 控制并发量可以手动设置线程的数量,避免线程过多而出现资源耗尽的问题
    • 简化线程管理避免手动管理线程的生命周期
  • 使用到一些函数,类以及关键字

    • std::result_of<>(C++11/C++14)以及 std::invoke_result<>(C++17 及以上)

      用来推导函数调用结果类型元编程工具

      template<typename _Fty, typename... _ArgsType>
      std::result_of<_Fty(_ArgsType ...)>::type;
      //_Fty函数返回值类型
      //_ArgsType函数参数类型
      
    • std::function<>

      C++ 标准库中的一个类模板用于封装可调用对象(如函数、lambda 表达式、绑定表达式、函数对象等)

      提供了一种统一的方式来存储和调用不同类型的可调用对象

      int add(int a, int b) {
          return a + b;
      }
      std::function<int(int, int)> func = add;
      
      std::queue<std::function<void()>> works_queue;
      
    • typename

      C++ 中的一个关键字,主要用于在模板中声明类型名称

      • 在模版参数列表中声明类型参数
      template<typename _Fty, typename... _ArgsType>//也可以使用class
      
      • 在嵌套类型中声明类型名称

        告知编译器这是一个类型名称而不是一个静态成员或值

      typename std::result_of<_Fty(_ArgsType ...)>::type
      
      • using 声明中声明类型别名
        • using可以用来创建类型别名,类似于 typedef,但语法更清晰和灵活
      using return_type = typename std::result_of<_Fty(_ArgsType ...)>::type;
      
    • std::make_shared<>

      C++ 标准库中的一个函数,用于创建并初始化一个 std::shared_ptr 对象。它提供了一种更高效和安全的方式来创建共享指针

      优点

      • 效率std::make_shared 通常比直接使用 newstd::shared_ptr 更高效。这是因为 std::make_shared 可以一次性分配对象和控制块(包含引用计数等信息)的内存,减少了内存分配的次数
      • 安全性std::make_shared 可以避免在对象创建过程中出现的异常安全问题。如果构造函数抛出异常,std::make_shared 会确保已经分配的内存被正确释放,避免内存泄漏
    auto task_ptr = std::make_shared<std::packaged_task<return_type()>>
    	(std::bind(std::forward<_Fty>(fnarg), std::forward<_ArgsType>(args)...));
    /*
    必须使用共享指针
    
    自动内存管理:std::shared_ptr 提供了自动内存管理功能。当最后一个引用释放时,它会自动删除所管理的对象。这对于多线程环境特别重要,因为多个线程可能同时访问同一个任务对象
    
    避免悬挂指针:在多线程环境中,如果使用裸指针或局部对象,可能会出现悬挂指针的问题,即某个线程在其他线程还在使用该对象时就销毁了它。使用 std::shared_ptr 可以确保任务对象在所有相关线程完成操作之前不会被销毁
    
    原子引用计数:std::shared_ptr 的引用计数是原子的,这意味着在多线程环境中可以安全地增减引用计数,而不需要额外的同步机制
    
    */
    
  • std::bind()

    C++ 标准库中的一个函数,用于创建一个可调用对象,该对象可以将函数、成员函数或函数对象与其参数绑定在一起

    • 可以使用占位符

      C++ 标准库提供了一些占位符,位于 std::placeholders 命名空间中:

      • _1:表示第一个参数
      • _2:表示第二个参数
      • _3:表示第三个参数
      • 依此类推
    auto lambda = [](int a, int b) { return a + b; };
    auto bound_lambda = std::bind(lambda, 3, 4);
    
    class MyClass {
    public:
        void print_value(int value) const {
            std::cout << "Value: " << value << std::endl;
        }
    };
    void test()
    {
    	MyClass obj;
    	auto bound_member_func = std::bind(&MyClass::print_value, &obj, std::placeholders::_1);
    	bound_member_func(42);
    }
    
    auto task_ptr = std::make_shared<std::packaged_task<return_type()>>
    	(std::bind(std::forward<_Fty>(fnarg), std::forward<_ArgsType>(args)...));
    

4.1 静态线程池

线程池内线程数目保持不变

  • 创建思路

    • 构造函数
      1. 确定初始线程数目
      2. 创建一定数量的线程
    • 加入任务队列函数
      1. 将传入函数打包进入队列等待
      2. 对阻塞线程进行唤醒
    • 执行任务函数
      1. is_stop 1/0, empty() 1/0
        • 0, 0通过,线程继续执行任务
        • 0, 1阻塞
        • 1, 0通过,线程继续执行任务
        • 1, 1通过,线程退出(工作线程退出位置)
      2. 可以执行任务的线程执行任务
      3. 执行完进入下一轮循环1
    • 析构函数
      1. 更新is_stop为true,并且唤醒所有阻塞线程
      2. 回收所有线程资源(工作线程资源回收位置)
  • 头文件

#pragma once

#include<functional>
#include<atomic>
#include<vector>
#include<queue>
#include<unordered_map>
#include<future>
#include<iostream>
#include<mutex>
#include<condition_variable>

class StaticThreadPool
{
public:
	StaticThreadPool(int threads_number);

	template<typename _Fty, typename... _ArgsType>			//有返回值的函数
	auto add_task_(_Fty&& fnarg, _ArgsType&&... args) -> std::future<typename std::result_of<_Fty(_ArgsType ...)>::type>;

	void add_task_void(std::function<void()>);			//没有返回值的函数

	~StaticThreadPool();

private:
	void work();			//线程获取任务并执行

	bool is_stop;				//停止标志	运行false	停止true

	int threads_min;			//最小线程数目
	int threads_max;			//最大
	std::atomic<int> threads_current;		//当前存在

	std::mutex queue_mutex;			//队列互斥锁

	std::condition_variable queue_cond;			//等待队列条件变量
	std::condition_variable return_join_cond;			//线程先退出后回收条件变量


	std::vector<std::thread> works;			//存储当前线程
	std::unordered_map<std::thread::id, std::thread> works_map;			//可以存储线程id以及当前线程
	std::queue<std::function<void()>> works_queue;			//等待队列

};
  • 成员函数实现
StaticThreadPool::StaticThreadPool(int threads_number) : is_stop(false), threads_min(threads_number), threads_max(std::thread::hardware_concurrency()),
threads_current(threads_number)
{
	std::cout << "max threads number is " << threads_max << std::endl;
    
	if (threads_number > threads_max)	//限定线程数目最大值
	{
		threads_number = threads_max;
		std::cout << "threads number exceeds the limit, set the number to max" << std::endl;
	}
	else if (threads_number <= 0)
		throw std::runtime_error("The number of threads must be initialized to a number greater than zero.");

	for (size_t i = 0; i < threads_number; i++)		//创建工作线程
	{
		成员函数创建线程需要使用指针
		//works.emplace_back(std::thread([this]() {			创建不带线程id的
		//	this->work();
		//	}));
		//
		//std::cout << " + create a thread to work" << std::endl;

		std::thread temp_thread([this]() { this->work(); });			//创建带有线程id的
		works_map.emplace(temp_thread.get_id(), std::move(temp_thread));

		std::cout << " + create " << i + 1 << " thread to work" << std::endl;
	}
}

StaticThreadPool::~StaticThreadPool()
{
	std::unique_lock<std::mutex> queue_lock(queue_mutex);
	is_stop = true;
	queue_lock.unlock();

	queue_cond.notify_all();

	//for (std::thread& it : works)			//回收线程资源
	//{
	//	std::cout << " * join a thread" << std::endl;
	//	it.join();
	//}

	queue_lock.lock();			//实现先return后join
	while (threads_current != 0)
	{
		return_join_cond.wait(queue_lock);
	}
	for (auto& it : works_map)
	{
		std::cout << " * join thread " << it.first << std::endl;
		it.second.join();
	}

}

template<typename _Fty, typename... _ArgsType>
auto StaticThreadPool::add_task_(_Fty&& fnarg, _ArgsType&&... args) -> std::future<typename std::result_of<_Fty(_ArgsType ...)>::type>
{
	using return_type = typename std::result_of<_Fty(_ArgsType ...)>::type;

	auto task_ptr = std::make_shared<std::packaged_task<return_type()>>			//使用共享指针保证std::packaged_task 的生命周期足够长
		(std::bind(std::forward<_Fty>(fnarg), std::forward<_ArgsType>(args)...));

	std::future<return_type> result_future = task_ptr->get_future();

	std::unique_lock<std::mutex> lock(queue_mutex);
	works_queue.emplace([task_ptr]() {			//将打包好的函数加入等待队列
		(*task_ptr)();
		});
	lock.unlock();

	queue_cond.notify_one();			//通知阻塞的工作线程

	return result_future;
}

void StaticThreadPool::add_task_void(std::function<void()> fnarg)
{
	std::unique_lock<std::mutex> lock(queue_mutex);
	works_queue.emplace(fnarg);			//将打包好的函数加入等待队列
	lock.unlock();

	queue_cond.notify_one();			通知阻塞的工作线程
}

void StaticThreadPool::work()
{
	while (true)
	{
		std::function<void()> task = nullptr;			//用于接收任务函数

		std::unique_lock<std::mutex> lock(queue_mutex);	
		while (!is_stop && works_queue.empty())
		{
			queue_cond.wait(lock);
		}
		if (is_stop && works_queue.empty()) {			//满足条件使线程退出
			std::cout << " - return thread " << std::this_thread::get_id() << std::endl;

			threads_current--;			//实现先return后join
			if (threads_current == 0) return_join_cond.notify_one();

			return;
		}

		task = this->works_queue.front();			//弹出任务并接收任务
		works_queue.pop();
		lock.unlock();

		task();			//执行任务
		std::cout << "thread " << std::this_thread::get_id() << " had down a work" << std::endl;
	}

}
  • 测试样例
#include"my_threadpool.hpp"

const int task_quantity = 10;

int main()
{
	std::vector<std::future<int>> future_result;

	StaticThreadPool my_threadpool(4);

	for (int i = 0; i < task_quantity; i++)
	{
		future_result.emplace_back(std::future<int>(my_threadpool.add_task_([](int a, int b) -> int {
			std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
			return a + b;
			}, i + task_quantity, i + task_quantity)));
	}

	for (int i = 0; i < task_quantity; i++)
	{
		my_threadpool.add_task_void([i]() {
			std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
			std::cout << "void function out put " << i << std::endl;
			});
	}

	//system("pause");

	for (auto& it : future_result)
	{
		std::cout << "This main thread " << std::this_thread::get_id() << " future_value " << (it).get() << std::endl;
	}

	std::cout << std::thread::hardware_concurrency() << std::endl;

	return 0;
}

4.2 动态线程池

线程池内线程数目进行动态的增加和减少

主要是通过管理者线程进行线程的增加以及减少

  • 创建思路

    • 构造函数
      1. 确定初始线程数目
      2. 创建管理者线程
      3. 创建一定数量的线程
    • 加入任务队列函数
      1. 将传入函数打包进入队列等待
      2. 对阻塞线程进行唤醒
    • 执行任务函数
      1. is_stop 1/0, empty() 1/0
        • 0, 0通过,线程继续执行任务
        • 0, 1阻塞,当条件满足从中取出部分线程进行删除(删除空闲线程)
        • 1, 0通过,线程继续执行任务
        • 1, 1通过,线程退出(工作线程退出位置)
      2. 可以执行任务的线程执行任务
      3. 执行完进入下一轮循环1
    • 管理者线程
      1. 每隔一段时间进行空闲线程数量的检查
      2. 空闲线程较多则通知删除线程,等待回收删除线程资源(回收删除的空闲线程资源)
      3. 空闲线程较少则增加线程
      4. 当线程池停止管理者线程退出
    • 析构函数
      1. 更新is_stop为true,并且唤醒所有阻塞线程
      2. 回收所有线程资源(工作线程资源回收位置)
      3. 回收管理者线程资源
  • 头文件

class DynamicThreadPool
{
public:
	DynamicThreadPool(int threads_number);

	template<typename _Fty, typename... _ArgsType>			//有返回值的函数
	auto add_task_(_Fty&& fnarg, _ArgsType&&... args) -> std::future<typename std::result_of<_Fty(_ArgsType ...)>::type>;

	void add_task_void(std::function<void()>);			//无返回值的函数

	~DynamicThreadPool();

private:
	int default_min_threads = 4;			//默认最小初始化线程数目

	void work();			//工作函数
	void manager();			//管理者函数

	std::thread* manager_thread; //管理者线程

	bool is_stop;				//停止标志	运行false	停止true

	int threads_min;			//最小线程数目	默认为default_min_threads
	int threads_max;			//最大			默认为std::thread::hardware_concurrency()
	std::atomic<int> threads_current;		//当前存在
	std::atomic<int> threads_idle;			//空闲
	std::atomic<int> threads_exit;			//需要删除

	std::mutex queue_mutex;			//等待队列互斥锁
	std::mutex thread_manager_mutex;			//线程管理互斥锁

	std::condition_variable queue_cond;			//等待队列条件变量
	std::condition_variable return_join_cond;			//线程先退出后回收条件变量
	std::condition_variable idle_delete_cond;			//空闲线程删除条件变量

	std::unordered_map<std::thread::id, std::thread> works_map;			//可以存储线程id以及线程
	std::queue<std::function<void()>> works_queue;			//任务等待队列
	std::vector<std::thread::id> idle_works_id;			//存储需要删除的空闲线程id

};
  • 成员函数实现
DynamicThreadPool::DynamicThreadPool(int threads_number) : is_stop(false), threads_min(default_min_threads), threads_max(std::thread::hardware_concurrency()),
threads_current(threads_number), threads_idle(threads_number), threads_exit(0)
{
	if (threads_number > threads_max)	//限定线程数目最大值
	{
		threads_number = threads_max;
		threads_current = threads_max;
		threads_idle = threads_max;

		std::cout << "threads number exceeds the limit, set the number to max " << threads_max << std::endl;
	}
	else if (threads_number < default_min_threads)
	{
		std::cout << "The number of threads must be initialized to a number greater than or equal to " << default_min_threads << std::endl;
		throw std::runtime_error("");
	}

	manager_thread = new std::thread([this]() {			//创建管理者线程,需要使用指针创建
		this->manager();
		});

	for (size_t i = 0; i < threads_number; i++)		//创建带有线程id的工作线程
	{
		std::thread temp_thread([this]() {
			this->work();
			});
		works_map.emplace(temp_thread.get_id(), std::move(temp_thread));

		std::cout << "Initialize " << i + 1 << " thread prepare to work" << std::endl;
	}
}

DynamicThreadPool::~DynamicThreadPool()
{
	std::unique_lock<std::mutex> queue_lock(queue_mutex);
	is_stop = true;
	queue_lock.unlock();

	queue_cond.notify_all();

	queue_lock.lock();			//实现先return后join
	while (threads_current != 0)
	{
		return_join_cond.wait(queue_lock);
	}

	for (auto& it : works_map)
	{
		if ((it).second.joinable())			//避免join管理者线程已经join过的线程
		{
			std::cout << " ****** join thread " << it.first << std::endl;
			it.second.join();
		}
	}

	std::cout << " ****** Manager thread " << (*manager_thread).get_id() << " joined " << std::endl;
	(*manager_thread).join();			//join管理者线程
	
}

template<typename _Fty, typename... _ArgsType>
auto DynamicThreadPool::add_task_(_Fty&& fnarg, _ArgsType&&... args) -> std::future<typename std::result_of<_Fty(_ArgsType ...)>::type>
{
	using return_type = typename std::result_of<_Fty(_ArgsType ...)>::type;

	auto task_ptr = std::make_shared<std::packaged_task<return_type()>>			//使用共享指针保证std::packaged_task 的生命周期足够长
		(std::bind(std::forward<_Fty>(fnarg), std::forward<_ArgsType>(args)...));

	std::future<return_type> result_future = task_ptr->get_future();

	std::unique_lock<std::mutex> lock(queue_mutex);
	works_queue.emplace([task_ptr]() {			//将打包好的函数加入等待队列
		(*task_ptr)();
		});
	lock.unlock();

	queue_cond.notify_one();//通知1,任务队列+1

	return result_future;
}

void DynamicThreadPool::add_task_void(std::function<void()> fnarg)
{
	std::unique_lock<std::mutex> lock(queue_mutex);
	works_queue.emplace(fnarg);			//将打包好的函数加入等待队列
	lock.unlock();

	queue_cond.notify_one();//通知1,任务队列+1
}


void DynamicThreadPool::manager()
{
	while (true)
	{
		std::this_thread::sleep_for(std::chrono::seconds(2));			//每隔一段时间进行检测

		std::unique_lock<std::mutex> lock(thread_manager_mutex);

		while (threads_exit != 0)//1当存在需要删除的线程时等待
		{
			idle_delete_cond.wait(lock);
		}

		for (auto& it : idle_works_id)			//空闲线程较多则通知删除线程,等待回收删除线程资源
		{
			auto idle_thread = works_map.find(it);
			if (idle_thread != works_map.end())
			{
				(*idle_thread).second.join();
				std::cout << " ===*** join thread " << it << std::endl;
			}
		}
		idle_works_id.clear();//回收后清空之前存储的应该删除的线程id
		idle_delete_cond.notify_all();//通知2

		if (threads_idle > threads_current / 2 && threads_current > threads_min + 2)//更新需要删除的线程数量
		{
			threads_exit.store(2);
			queue_cond.notify_all();//通知1
		}

		if (threads_idle == 0 && threads_current <= threads_max - 1)			//空闲线程较少则增加线程
		{
			for (int i = 0; i < 1; i++)
			{
				threads_current++;
				threads_idle++;

				std::thread new_thread([this]() {
					this->work();
					});
				std::cout << " ++++++ create a new " << new_thread.get_id() << " thread to work" << std::endl;
				works_map.emplace(new_thread.get_id(), std::move(new_thread));
			}
		}

		if (is_stop)			//当线程池停止则退出管理者线程
		{
			std::cout << " ------ Manager thread " << std::this_thread::get_id() << " returned " << std::endl;
			return;
		}
	}
}

void DynamicThreadPool::work()
{
	while (true)
	{
		std::function<void()> task = nullptr;//用于接收任务函数

		std::unique_lock<std::mutex> lock(queue_mutex);
		while (!is_stop && works_queue.empty())
		{
			queue_cond.wait(lock);//1阻塞满足条件的线程

			std::unique_lock<std::mutex> lock1(thread_manager_mutex);
			if (!is_stop && threads_exit.load())			//当条件满足从中取出部分线程进行删除
			{
				threads_exit--;
				threads_current--;

				idle_delete_cond.notify_all();//通知1

				idle_works_id.emplace_back(std::this_thread::get_id());
				std::cout << " ===--- return idle thread " << std::this_thread::get_id() << std::endl;
				return;
			}
		}
		if (is_stop && works_queue.empty()) {			//当线程池停止删除剩余线程

			std::unique_lock<std::mutex> thread_lock(thread_manager_mutex);
			while (!idle_works_id.empty())//2当存在需要回收的线程时等待
			{
				idle_delete_cond.wait(thread_lock);
			}

			threads_current--;
			return_join_cond.notify_one();

			std::cout << " ------ return thread " << std::this_thread::get_id() << std::endl;
			return;
		}

		task = this->works_queue.front();
		works_queue.pop();
		lock.unlock();

		threads_idle--;//该线程执行任务,空闲线程-1
		task();
		threads_idle++;//该线程完成任务,空闲线程+1
		std::cout << "thread " << std::this_thread::get_id() << " had down a work" << std::endl;
	}

}
  • 测试样例
#include"my_threadpool.hpp"

const int task_quantity = 100;

int main()
{
	std::vector<std::future<int>> future_result;

	DynamicThreadPool my_threadpool(5);

	for (int i = 0; i < task_quantity; i++)
	{
		future_result.emplace_back(std::future<int>(my_threadpool.add_task_([](int a, int b) -> int {
			std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
			std::this_thread::sleep_for(std::chrono::seconds(1));
			return a + b;
			}, i + task_quantity, i + task_quantity)));
	}

	for (int i = 0; i < task_quantity; i++)
	{
		my_threadpool.add_task_void([i]() {
			std::cout << "This child thread " << std::this_thread::get_id() << std::endl;
			std::cout << "void function out put " << i << std::endl;
			});
	}

	system("pause");

	for (auto& it : future_result)
	{
		std::cout << "This main thread " << std::this_thread::get_id() << " future_value " << (it).get() << std::endl;
	}

	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我还蒙在鼓里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值