0%

C++(七)-线程

一、线程基本概念

1、进程与线程区别

进程:系统中程序执行和资源分配的基本单位 。每个进程有自己的数据段、代码段和堆栈段 。 进程之间切换的话,需要进行上下文的切换,开销比较大。

线程:线程是操作系统能够进行运算调度的最小单位 。每个进程至少都有一个 main线程, 它与同进程中的其他线程共享进程空间:堆代码、数据 文件描述符、信号等 , 只拥有少量的栈空间, 大大减少了上下文切换的开销。线程也将其称为轻量级的进程。

线程和进程在使用上各有优缺点: 线程执行开销小, 占用的 CPU 少,线程之间的切换快,但不利于资源的管理和保护 而进程正相反 。

内核空间:被所有的进程所共享的。

用户态空间:每个进程在运行的时候,都会有自己的用户态空间。

2、线程的分类

用户线程:运行在用户态的线程,称为用户线程。

内核线程:运行在内核态的线程,称为内核线程。

用户态切换到内核态,可以使用系统调用。

image-20240129092639388

二、线程的常用函数

NPTL线程库

1、线程的创建(==重要==)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//thread:第一个参数,传递的是线程id。
//attr:第二个参数,是线程的属性。用默认属性,将其设置为nullptr/NULL
//start_routine:第三个参数,线程的入口函数,是函数指针。void *可以看成是C语言中的广泛类型。
//什么叫做线程入口函数:线程运行起来的时候,肯定需要去执行任务,那执行任务的的入口在哪里了,就是线程入口函数
//arg:第四个参数。线程需要进行传递的参数

//函数的返回值:是一个int类型的。如果函数的返回结果是0,那么就表明创建成功,否则创建失败就会返回错误码。

//编译的时候,需要带上-pthread或者-lpthread Compile and link with -pthread

image-20240129094135344

image-20240129104045288

image-20240129104603520

2、线程的退出

1
2
3
4
#include <pthread.h>
//让线程主动退出
void pthread_exit(void *retval);
//retval:可以将子线程中的值带出来。这个值,平时不需要的话,是可以不用传递

3、线程的等待(==重要==)

1
2
3
4
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//thread:需要等待的线程id
//retval:需要等待的(被等待的)线程的中需要传递出来的值

image-20240129110528646

4、获取线程id

1
2
#include <pthread.h>
pthread_t pthread_self(void);

image-20240129111126368

5、线程的取消(杀死)

1
2
3
4
#include <pthread.h>

int pthread_cancel(pthread_t thread);
//thread:被杀死(被取消)线程的线程id

image-20240129112636247

取消的时候,有一个取消点的概念。哪些函数具备取消点,可以使用man 7 pthreads查看。

三、面向对象的线程封装

1、类图

image-20240129151552655

2、代码难点

2.1、threadFunc必须是静态的

image-20240129151642406

2.2、传递this指针

image-20240129151831366

四、基于对象的线程封装

1、类图

image-20240129161244114

2、代码难点

2.1、回调函数的注册

image-20240129161321564

image-20240129161337618

2.2、回调函数的执行

image-20240129161444181

五、互斥锁

1、互斥锁的类型

1
2
#include <pthread.h>
pthread_mutex_t mutex;//可以创建互斥锁的变量

2、互斥锁的初始化

1
2
3
4
5
6
7
8
9
#include <pthread.h>
//静态初始化
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

//动态初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
//mutex:传递互斥锁的变量
//mutexattr:互斥锁的属性,可以使用默认属性,nullptr/NULL
//返回值:返回类型是一个int。成功返回0,失败返回非0

3、互斥锁的销毁

1
2
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

4、互斥锁的上锁与解锁

1
2
3
4
5
6
7
8
9
#include <pthread.h>
//互斥锁的上锁操作
int pthread_mutex_lock(pthread_mutex_t *mutex);

//互斥锁的尝试上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//互斥锁的解锁操作
int pthread_mutex_unlock(pthread_mutex_t *mutex);

==总结:互斥锁的函数,成功返回的是0,失败返回的是非0的值,以及错误码。==

image-20240129171425699

image-20240129171948519

image-20240129173132368

image-20240129173205004

互斥锁使用总结:

1、上锁一定要注意解锁,不然有可能会发生阻塞状态(也就上锁与解锁一定要成对出现)

2、两次上锁,会让程序处于阻塞状态,但是可以多次执行尝试上锁(也要注意,一定要解锁)

3、锁只有上锁与解锁状态,没有其他状态

4、如果锁处于上锁状态,那么是不能进行销毁的。也就是只有处于解锁状态的时候,才能销毁。

六、条件变量

1、条件的类型

1
2
#include <pthread.h>
pthread_cond_t cond;//可以创建条件变量的变量

2、条件变量的初始化

1
2
3
4
5
6
7
8
9
#include <pthread.h>
//静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//动态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//cond:传递条件变量的变量
//cond_attr:条件变量的属性,可以使用默认属性,nullptr/NULL
//返回值:返回类型是一个int。成功返回0,失败返回非0

3、条件变量的销毁

1
2
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);

4、条件变量的等待

1
2
3
4
5
6
7
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//cond:条件变量对应的变量
//mutex:需要互斥锁的对象

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);

5、条件变量的通知

image-20240129174951705

1
2
3
4
5
//至少唤醒一个等待等待在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);

//唤醒在条件变量上的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

==总结:条件变量的函数,成功返回的是0,失败返回的是非0的值,以及错误码==

一、问题回顾

1、线程与进程的区别?

2、线程的常用函数?

3、面向对象线程封装的设计与代码难点?

4、基于对象的线程封装的设计与代码难点?

5、互斥锁的常用函数以及所用需要注意那些点?

6、条件变量的常用函数有哪些?

二、条件变量的使用

image-20240130095511622

唤醒操作一定要在wait之后,不然wait在条件变量上的线程是无法被唤醒的。

三、面向对象生产者与消费者封装

1、原理图

image-20240130104138249

2、类图设计

image-20240130114602958

3、代码难点(==重要==)

3.1、防止死锁

image-20240130160430608

image-20240130160446429

image-20240130160551421

3.2、虚假唤醒

image-20240130160742572

3.3、TaskQueue中的push与pop完整思路

image-20240130160832357

image-20240130160911093

3.4、生产者与消费者的run方法

image-20240130161014148

image-20240130161045709

3.5、禁止复制

image-20240130161938422

image-20240130161956306

image-20240130162015124

一、问题回顾

1、面向对象的PC原理?类图的设计是什么?

2、如何防止死锁?如何防止虚假唤醒?禁止复制有哪些方式?

3、如何解析源码?

4、基于对象的PC如何实现?

5、面向对象线程池的类图设计回顾?

二、基于对象的PC

1、类图

image-20240131090309971

2、代码难点

2.1、生产者与消费者的run的改造

image-20240131090429077

image-20240131090525191

2.2、测试代码

image-20240131090851919

三、线程池

三、面向对象线程池封装

1、使用场景

如果任务比较少的话,可以来一个任务就对应创建一个线程,当任务执行结束之后,就将线程回收。如果任务量比较大的时候,还这样操作,那么线程的创建与销毁就比较频繁,而线程的创建与销毁也是会耗费资源的,所以在任务来之前,就创建一部分线程,任务到来之后,就放在任务队列中,线程池中的子线程在任务队列中拿任务,只要任务没有执行完毕,那么子线程就一直拿任务,执行任务。

2、类图的设计

image-20240131121341333

3、代码难点

3.1、任务执行不完,程序就退出了

image-20240131121450861

image-20240131121541742

3.2、线程池无法退出

image-20240131121624614

image-20240131121722958

image-20240131121842007

image-20240131121947069

image-20240131122158527

4、面向对象的线程池序列图

image-20240201093230883

一、基于对象的线程池封装

1、类图设计

image-20240201094211788

2、代码难点

2.1、doTask的注册与执行

image-20240201103140624

image-20240201103241431

image-20240201103332931

2.2、MyTask类中process的注册与执行

image-20240201103525474

image-20240201103618504

image-20240201103804930

-------------本文结束感谢您的阅读-------------