文章目录
1.0 Linux线程概念
1.1 Linux线程基本概念
操作系统中的线程:是在进程内部运行的一个执行分支(执行流),属于进程的一部分, 粒度要比进程更细更轻量化
今天的进程vs之前的进程:之前的进程,内部只有一个执行流的进程今天的进程,内部可以有多个执行流
内部:线程在进程的地址空间中运行
执行分支:CPU在调度的时候只看PCB,每一个PCB被指派过指向方法和数据。CPU可以直接被调度
一个进程内存在多个线程,OS使用先描述后组织的方式管理线程,在Windows为代表的操作系统中使用线程控制块TCB来管理线程。Linux为代表的操作系统中并没有为线程设计专门的TCB,而是使用进程的PCB来模拟线程
创建线程只创建task_struct(PCB),这些PCB共享同一个地址空间,并将当前进程的代码和数据进行了划分
1.2 Linux线程优劣介绍
Linux PCB模拟线程的优势
.不用模拟复杂的进程线程关系,不用单独为线程设计任何算法,直接使用进程的一套相关方法。OS只需要聚焦于进程间的资源分配上就可以了
计算密集型应用:加密,大数据运算等主要使用CPU资源应用。多线程的计算密集型应用可以在多个CPU上进行运算,以达到提高效率的目的。但若是线程数大于CPU数,则会带来线程切换的损失,所以不建议线程数过多
I/O密集型应用:网络游戏,直播,电影等主要使用内存和外设的IO资源的应用。内存与外设的I/O操作速度远远小于CPU计算速度,CPU的大量时间是在等待。所以可以一个CPU上多个线程进行运算以达到CPU不断运算提高效率。当然若线程太多则会导致线程切换带来很大的性能损失
Linux PCB模拟线程的劣势
1.Linux下线程因为是使用进程模拟的,所以Linux下不会给我们提供直接操作线程的接口,而是给我们提供,在同一个地址空间内创建PCB的方法,分配资源给指定PCB的接口,对用户非常不友好,(释放/等待/创建线程的接口)由系统级别工程师,在用户层对Linux轻量级进程接口进行封装,给我们打包成库,让用户直接使用库接口,原生线程库(用户层)
线程优势
1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程劣势
1.健壮性降低:线程崩溃会导致其他线程一并崩溃,甚至进程崩溃。编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了,不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
2.缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
3.编程难度提高比:编写调试一个多线程远比单线程难
所以当我们使用C语言想要使用使用多线程的库时,需要手动链接线程库。在C++11提供了线程库
创建进程:创建PCB,创建进程地址空间,创建页表,并将代码和数据加载进内存等
创建线程:创建PCB,OS将进程的一部分代码和数据分配给线程
结论:创建进程的"成本(时间+空间)"非常高,创建进程要使用的资源非常多
内核视角看进程和线程:
进程是承担分配系统资源的基本实体,进程划分资源给线程
线程是CPU调度的基本单位,承担进程资源的一部分的基本实体
Linux线程与接口关系的认识Linux PC<=传统意义上的PCBLinux进程(轻量级进程)
1.0S创建"线程" 2.CPU调度
所有的轻量级进程(“线程”)都是在进程中(进程地址空间)运行
进程,线程对资源的管理
进程:地址空间独立,大部分资源独享,小部分共享(管道、ipc资源)
线程:地址空间共享,大部分资源共享,小部分独有(pcb,栈,上下文)
小测试:进程创建,PID, LWP介绍
指令:ps -aL:显示当前系统进程状态信息,并显示执行流信息
选项:-lpthread 链接原生库
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* thread_code(void* args)
{
const char* id = (const char*)args;
while (1){
printf("我是%s, mypid = %d\n", id, getpid());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_code, (void*)"child thread");
while (1){
printf("我是main 线程, mypid = %d\n", getpid());
sleep(1);
}
}
测试结果
可以看到两个线程的PID是相同的,但是后面的LWP轻量级进程(light weight process)编号却不同。
Linux OS进行调度的时候,看的是LWP(在单线程情况下,PID=LWP>, main线程的PID=LWP)
2.0 Linux线程控制
2.1 pthread_create(创建线程)
第一个参数 thread
pthread_t 是由usigned long typedef 来的,这个参数是一个输出型参数,在用户层面用于接收系统分配给我们的线程编号
第二个参数 attr
设置线程属性,大部分情况下传NULL让系统帮助我们默认设置
第三个参数 start_routine
参数为void* ,返回值也为void* 的函数指针,新创建的线程会执去行这个函数
第四个参数 arg
会作为实参传递给start_rotine的形参
pthread_self()函数, 获取所在线程的tid
测试1:pthread_create基本使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* thread_name)
{
while (1){
printf("I'm %s, my tid = %lu\n", thread_name, pthread_self());
sleep(1);
}
}
void test1()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"child thread");
while (1){
printf("I'm main thread, my tid = %lu\n", pthread_self());
sleep(1);
}
}
int main()
{
test1();
}
实验结果
测试2:线程的健壮性
[clx@VM-20-6-centos thread_test]$ cat mytest.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define THD_NUM 5
void* thread_run(void* thread_index)
{
int index = *(int*)thread_index;
while (1){
printf("child thread[%d], tid = %lu\n", index, pthread_self());
if (*(int*)thread_index == 2){ //第三个线程符合条件,执行异常代码
printf("%d", 1 / 0);
}
sleep(1);
}
}
void test2()
{
pthread_t tid_arr[THD_NUM];
for (int i = 0; i < THD_NUM; i++){
pthread_create(&tid_arr[i], NULL, thread_run, (void*)&i); //每两秒创建一个新线程
sleep(2);
}
}
int main()
{
test2();
}
实验结果
创建第三个线程打印一句话后执行异常代码后进程崩溃。说明在多线程的进程中只要有一个线程出现问题很可能会影响到其他进程,甚至导致进程崩溃
2.2 pthread_join(线程等待)
一般而言,线程也是需要被等待的,如果不等待,可能会导致类似于僵尸”进程“的问题。并且线程等待必须要一个一个进行阻塞式等待
线程退出会返回一个void 类型的值,我们可以通过这个值来判定线程跑完结果是否正确。*
线程异常情况是无法用线程的返回值进行判断的,因为线程异常则进程也一定异常,处理异常不属于线程层面管理范围,会由进程层面处理
第一个参数 thread
需要等待线程的tid
第二个参数
输出型参数,获取等待线程的退出码
线程退出码:子线程去执行的函数有一个void的返回值就是该线程退出码,想要获取void的变量,则需要传void**来进行接收
函数返回值
成功返回0,失败返回错误码
测试:接收线程退出信息
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define THD_NUM 5
typedef struct thread_info{ //使用结构体来封装进程退出的信息
int _number;
pthread_t _tid;
}thread_info;
void* thread_run(void* args)
{
int num = *(int*)args;
printf("我是新线程[%d], my tid = %lu \n", num, pthread_self());
sleep(6);
thread_info* info = (thread_info*)malloc(sizeof(thread_info));
info->_number = num;
info->_tid = pthread_self();
return info;
}
int main()
{
pthread_t tid_arr[THD_NUM];
//创建线程
for (size_t i = 0; i < THD_NUM; i++){
pthread_create(tid_arr + i, NULL, thread_run, (void*)&i);
sleep(1);
}
//打印线程信息
printf("#################begin#########################\n");
for (size_t i = 0; i < THD_NUM; i++){
printf("我创建的 %lu 子进程 tid = %lu\n", i, tid_arr[i]);
}
printf("################# end ########################\n");
//回收线程退出信息
thread_info* info_arr[THD_NUM];
for (size_t i = 0; i < THD_NUM; i++){
pthread_join(tid_arr[i], (void**)(info_arr + i));
}
//打印子线程信息
for (size_t i = 0; i < THD_NUM; i++){
printf("%lu 号子线程 number = %d, tid = %lu\n", i, (info_arr[i])->_number, (info_arr[i])->_tid);
}
sleep(100);
return 0;
}
在上述测试中,我们使用一个结构体来封装进程的信息,并使用这个结构体的二级指针来对其进行接收。以后我们也可以使用相同的方法对线程内容进行提取,返回给进程来对线程运行结果进行判断
2.3 pthread_exit(线程终止)
线程终止方案
1.函数中return(a.main 函数return代表主线程和进程退出) b. 其他线程return,只代表当前线程退出)
2.新线程通过pthread_exit终止自己
3.使用pthread_cancel()取消线程
exit是终止进程,所以不要在非主线程中调用
方法一:return
方法二:pthread_exit()
类似于main线程中的exit
第一个参数 retval
线程的返回值
方法三:pthread_cancel()
取消一个线程,被取消的线程退出码被设置成-1
第一个参数 thread
想要取消的线程的tid
返回值
成功返回0,失败返回错误码
测试1:使用pthread_cancel()取消子线程
void* thread_run(void* thread_name){
const char* name = (const char*)thread_name;
while (1){
printf("I'm %s\n", name);
sleep(1);
}
return(void*)123;
}
void test4(){
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"child thread");
sleep(5);
pthread_cancel(tid); //取消子线程
void* status = NULL;
pthread_join(tid, (void**)&status); //接收返回值
printf("%ld\n", status);
sleep(10);
}
int main()
{
test4();
return 0;
}
被取消的线程的返回值被设置成-1
测试2:使用pthread_cancel()取消主线程
void* thread_run(void* thread_name){
const char* name = (const char*)thread_name;
while (1){
printf("I'm %s\n", name);
sleep(1);
}
return(void*)123;
}
void test4(){
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"child thread");
sleep(5);
pthread_cancel(pthread_self()); //取消主线程
void* status = NULL;
pthread_join(tid, (void**)&status);
printf("%ld\n", status);
sleep(10);
}
int main()
{
test4();
return 0;
}
在主线程被取消后,监视窗口显示主线程已经死亡,但是子线程任然在运行。所以我们是可以使用其他线程来取消主线程的。但这个取消和主线程return有些不一样,取消主线程子线程并不会退出
2.4 pthread_detach(线程分离)
线程分离后的线程不需要被join,运行完毕,会自动释放Z状态的PCB。
一个线程被设置成分离之后,就不能对其进行等待了。主线程不退出,新线程处理完毕后自动退出
我们查看到的线程id是pthread库的线程id,不是Linux内核中的LWP,pthread库的线程id是一个内存地址
3.0 线程id和LWP的关系
通过线程库我们查看到的线程id 是pthread库的线程id,并非Linux内核中的LWP。pthread库的线程id实际上就是一个地址,这个地址可以帮助我们找到线程的用户级控制块以及线程的上下文数据和线程
每个线程都要有自己运行时的临时数据以及私有的栈结构。并且在用户看来进程可以创建终止等好像是独立的,所以也要有用户级的描述线程属性的用户及控制块。那么我们就可以将这三样东西打包封装到一个结构中,用数组存储到动态库中。所以我们只需要拿到动态库结构体的地址就可以拿到线程数据
线程ip和LWP的关系
pthread库:用户层描述线程属性和数据结构
每一个pthread_t tid 对应一个内核LWP,struct_pthread结构体中就存储了LWP,以便用户数据和内核数据进行交互
4.0 Linux线程互斥
因为多个线程是共享地址空间的,也就是很多资源是共享的
共享资源的优点:通信方便 缺点:缺乏访问控制
因为一个线程的操作问题,会给其他进程造成不可控的影响,或者引起崩溃,异常,逻辑不正确等现象。所以在多线程的编写过程中,我们时刻要注意线程安全
创建一个函数想要保证线程安全的话,不要使用全局变量,stl, malloc,new等会在全局内有效的数据(访问控制)
因为线程具有独立的栈结构,局部变量私有可以保证线程安全
为解决访问控制,线程引入互斥,同步
4.1 线程互斥相关概念
1.临界资源:凡是被线程共享访问的资源都是临界资源(多个线程、进程打印数据到显示器【临界资源】)
2.临界区:代码中访问临界资源的代码
3.方互斥或者同步:对临界区进行保护的功能,本质:就是对临界资源的保护
4.互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源),就可以称之为互斥
5.原子性:一个事情,要么不执行,要么执行完毕
6.同步:让访问临界资源的过程在安全的前提下(一般是互斥和原子的),让访问资源有一定的顺序性
测试1:线程非安全情况演示
#include <iostream>
#include <unistd.h>
#include <string>
//全局变量(临界资源)
int tickets = 1000;
void* thread_run(void* args)
{
int id = *(int*)args;
delete (int*)args;
while (true){
usleep(100);
std::cout << "我是[" << id << "]我抢到的票是:" << tickets << std::endl;
tickets--;
//当tickets小于零后结束抢票
if (tickets <= 0){
std::cout << "票抢完了" << std::endl;
break;
}
}
return nullptr;
}
int main()
{
pthread_t tid_arr[5] = {0};
//创建五个子线程进行抢票操作
for (int i = 0; i < 5; i++){
int *id = new int(i);
pthread_create(tid_arr + i, NULL, thread_run, id);
}
for (int i = 0; i < 5; i++){
pthread_join(tid_arr[i], nullptr);
}
return 0;
}
实验现象
可以看到票抢到了负数,这是因为当票数为接近零的时间店内有多个进程在临界区对临界数据进行操作,导致逻辑问题
tickets–一行代码,CPU需要从内存中读取tickets,然后对tickets进行计算,然后重新填回内存中。每一个时间段内该线程都可能会被切走。假设当下线程计算完想要将555这个数据填写到内存中被切走。其他线程对tickets–,当再次切回原线程时,tickets已经是10了。然后这个线程通过保存的上下文数据将555填回tickets,代码又出现了错误
4.2 线程互斥接口介绍
1.pthread_mutex_init(初始化)
第一个参数 pthread_mutex_t
锁是一种类型,由这种类型创造出来的对象叫做锁,将锁的地址传给函数即可完成初始化
第二个参数
设置锁的属性,填NULL会设置默认属性
2. pthread_mutex_destroy(销毁释放锁)
3. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(创建并自动初始化锁)
主要用于全局锁,或者静态锁,无需手动初始化或者释放
4.pthread_mutex_lock(加锁)
5.pthread_mutex_unlock(解锁)
因为这里的参数都是锁指针,和初始化函数的相同,就不一 一介绍了
测试:互斥锁的基本使用
我们将临界数据tickets进行了封装,并且为这个结构体配备了一把锁,此时买票这个动作就具有原子性,一个进程开始买票时进行加锁,买票成功后再解锁,不会出现多个进程同时访问临界资源的情况,这就是互斥
#include <iostream>
#include <unistd.h>
#include <string>
class Tickets{
private:
int tickets;
pthread_mutex_t mtx; //添加一把互斥锁
public:
Tickets():tickets(1000){
pthread_mutex_init(&mtx, nullptr); //在构造函数调用函数中对锁进行初始化
}
bool GetTickets(){
pthread_mutex_lock(&mtx); //后续即将对tickets这种临界数据访问,即将进入临界区,加锁
if (tickets <= 0){
std::cout << "票抢完了" << std::endl;
pthread_mutex_unlock(&mtx); //买票结束,访问临界区结束解锁
return false;
}
usleep(100);
std::cout << "我是[" << pthread_self() << "]我抢到的票是:" << tickets << std::endl;
tickets--;
pthread_mutex_unlock(&mtx); //买票结束,访问临界区结束解锁
return true;
}
~Tickets(){
pthread_mutex_destroy(&mtx); //在析构函数中将锁释放
}
};
int tickets = 1000;
void* thread_run(void* args)
{
Tickets* ticket = (Tickets*)args;
while (true){
if (!ticket->GetTickets()){
break;
}
}
return nullptr;
}
int main()
{
Tickets *ticket = new Tickets(); //使用new创建临界资源(全局有效数据)
pthread_t tid_arr[5] = {0};
for (int i = 0; i < 5; i++){
pthread_create(tid_arr + i, NULL, thread_run, (void*)ticket); //将这个ticket对象指针传给每一个线程
}
for (int i = 0; i < 5; i++){
pthread_join(tid_arr[i], nullptr);
}
return 0;
}
使用互斥锁测试结果
将互斥锁的加锁和解锁函数屏蔽后测试结果
可以看到互斥锁对临界资源确实起到了访问保护的作用,成功预防了代码逻辑错误,保证线程安全
C++11也推出了语言级别的线程库 mutex, 锁类型即为 mutex, 可以直接调用lock(), 和unlock()函数进行加/解锁
4.3 线程互斥的基本原理
前提:我们认为一条汇编代码是原子的,而互斥锁原子性的实现就是靠一条汇编实现的
我们可以将锁想象成一个整形变量,初始值为1,线程竞争锁成功lock–, 释放锁后 lock++。进程想要加锁时进行判断
if (lock > 0) 如果成功说明锁别人还没有用,可以拿到lock–这个进程拿到锁,允许进入临界区执行后续代码,其他线程看到的lock就是0,将会被挂起。但是光是if (lock > 0)这一行代码就不是原子的,而锁的实际操作是由汇编实现的
lock:
movb $0, %al
xchgb %al, mutex//使用一行汇编,原子性完成共享数据mutex,交换到线程的上下问文中,实现私有化
if (al寄存器中的内容 > 0){
return 0;
}else{
挂起等待;
goto lock;
}
%al是一个CPU寄存器,mutex就是我们设置的锁,锁是具有全局属性的数据,所有线程都可以看见
CPU寄存器的数据:执行流的上下文数据,线程私有
movb $0, %al语句:设置每个线程的CPU寄存器信息(上下文数据),因为线程可能被不断切换,回来时需要装入自己的上下文数据
xchgb %al, mutex语句:交换mutex和寄存器中的数据
假设现在锁还未被竞争走,所以mutex = 1,线程A被CPU调度并且A代码中申请加锁。那么首先执行movb $0, %al,将A的上下文数据放入寄存器,因为此时还未加锁,所以寄存器中%al 为 0.然后A执行xchgb %al, mutex 因为这是一条汇编代码,在交换开始至结束不会被切换(原子性),交换完成后这个线程就等于拥有了这把锁,即便这个线程被切走,锁数据会被当作上下文数据一并被带走,其他线程看到的mutex为零,若想要申请锁就会被挂起,直到原线程归来执行完代码释放锁,然后其他线程才有机会进行加锁
线程挂起后被唤醒是有代价的,会消耗一定的时间,向我们上面的抢票代码,子线程一直在申请锁,得到锁的线程处于活跃状态,其他线程处于挂起状态,那么其他线程大概率竞争不过得到锁的线程,所以即使其释放锁后,大概率也是自己再次抢到锁,虽然这种情况保证了临界资源互斥,但是并不合理,所以访问控制还需要同步
4.4 其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作(再MySQL中会详细讲解)
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁:公平锁,非公平锁
自旋锁:对于悲观锁来说,若锁被人拿走则没有锁的线程则会被挂起等待,但是线程挂起是存在代价的,若拿到锁的线程执行代码段很短,可能其他线程还未挂起成功,它就把锁释放了。为了处理线程访问临界资源时间较短的情况,就有了自旋锁,它会通过不断的循环来检测锁的状态,使得线程不会被挂起,而如果访问临界资源时间较长,则比较适合挂起等待锁
自旋锁接口:
因为自旋锁的接口和挂起等待锁非常相似,就不再解释了
5.0 死锁
5.1 死锁基本概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资
源而处于的一种永久等待状态
如图所示:当一个执行流在执行insert函数代码,【这个函数添加互斥锁,保证只有一个执行流可以访问临界区,所以这个函数是可重入的】。当insert函数尚未释放锁时,信号来了。不巧的是自定义信号函数中也有一个insert()函数,但是由于此时锁已经被main执行流拿走了,所以执行信号的执行流被挂起了,并且不管其怎么等待锁也不会被释放,这种现象就叫死锁
函数是线程安全的,但是不一定可重入
那么一个执行流会不会产生死锁问题呢? 会
最简单的方式就是在一段代码中连续申请同一个锁,第一次申请锁已经被拿走了,第二次申请锁显示锁已经被拿走,这个执行流就被挂起了
5.2 死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
5.3 避免死锁的方法
1.破坏死锁的四个必要条件
2.加锁顺序一致
3.避免锁未释放场景
4.资源一次性分配
6.0 Linux线程同步
6.1 线程同步相关概念
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
竞态条件:线程挂起后被唤醒是有代价的,会消耗一定的时间,向我们上面的抢票代码,子线程一直在申请锁,得到锁的线程处于活跃状态,其他线程处于挂起状态,那么其他线程大概率竞争不过得到锁的线程,所以即使其释放锁后,大概率也是自己再次抢到锁,虽然这种情况保证了临界资源互斥,但是并不合理,所以访问控制还需要同步,保证线程访问具有一定的顺序性
6.2 线程同步接口介绍
1.pthread_cond_init(初始化)
第一个参数 cond
pthread_cond_t 是条件变量的类型,cond就是这种类型创建出的对象的地址
第二个参数 attr
可用于设置条件变量的属性,填NULL会设置默认属性
2. pthread_mutex_destroy(销毁释放条件变量)
3. pthread_cond_t cond = PTHREAD_COND_INITALIZER(创建并自动初始化条件变量)
主要用于全局或者静态条件变量,无需手动初始化或者释放
4.pthread_cond_wait
第一个参数 cond
一个条件变量对象的指针,标识函数正在等待哪一个条件就绪
第二个参数 mutex
一个互斥锁的指针,在线程wait时可能带有锁,所以将锁释放,以便其他线程在此线程wait时正常运作,若此线程条件满足会被唤醒,并且去竞争锁,拿到锁后执行后续代码
5.pthread_cond_broadcast
唤醒在此条件变量下等待的所有线程(广播唤醒所有)
6.pthread_cond_signal
唤醒在此条件变量下等待的一个线程
测试:同步接口的基本使用
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#define THD_NUM 3
//定义全局的锁和条件变量
pthread_mutex_t mtx;
pthread_cond_t cond;
void* boss_run(void* args){
std::string name = (char*)args;
while (true){
//唤醒在条件变量下的一个进程
std::cout << "master say begin work" << std::endl;
pthread_cond_signal(&cond); //唤醒条件变量下等待的一个线程
//pthread_cond_broadcast(&cond);
sleep(1);
}
}
void* worker_run(void* args){
int id = *(int*)args;
delete (int*)args;
while (true){
pthread_cond_wait(&cond, &mtx); //等待条件就绪
std::cout << "worker " << id << " is working ..." << std::endl;
sleep(1);
}
}
int main(){
//对锁和条件变量进行初始化
pthread_mutex_init(&mtx, NULL);
pthread_cond_init(&cond, NULL);
//创建一个master进程和 THD_NUL个worker进程
pthread_t master;
pthread_t tid_arr[THD_NUM];
pthread_create(&master, NULL, boss_run, (void*)"boss"); //让master去执行boss_run 函数
for (int i = 0; i < THD_NUM; i++){
int* number = new int(i);
pthread_create(tid_arr + i, NULL, worker_run, (void*)number); //让worker去执行 worker_run函数
}
pthread_join(master, NULL); //等待回收线程
for (int i = 0; i < THD_NUM; i++){
pthread_join(tid_arr[i], NULL);
}
pthread_mutex_destroy(&mtx); //销毁锁和条件变量
pthread_cond_destroy(&cond);
return 0;
}
实验现象
同一个条件变量下有三个进程在进行等待,谁先被唤醒是并不确定的,但是在后续运行的过程中,唤醒顺序遵照第一组的顺序,我们可以得出一个结论,条件变量内部一定存在一个等待队列
我们可以将条件变量堪称一个结构体,内部有两个成员变量,一个int status来衡量条件是否就绪,还有一个task_struct* 类型的指针,将所有等待的线程的PCB链接成等待队列,一个个有次序的唤醒
6.3 生产者消费者模型
三种关系,两种角色,一个场所
只有生产之知道什么时候该消费,只有消费者知道什么时候该生产。消费者看到超市里的货物要卖完了唤醒生产者进货。生产者看到超市里货物满了或者很多时,唤醒消费者来进行消费
优势:将生产环节和消费环节进行解耦,支持并发,使生产消费同步进行, 减少交易成本
通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接进行通信,而是通过一个场所(阻塞队列,环形对垒等)来进行通信,所以生产者生产完数据不需要等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接去阻塞队列中获取,阻塞对垒就相当一个缓冲区,平衡了消费者和生产者之间的效率差异,将生产消费解耦
6.4 基于阻塞队列的生产消费模型实现
三种关系
生产者和生产者 互斥(使用互斥锁实现)
消费者和消费者 互斥(使用互斥锁实现)
生产者和消费者 同步 和 互斥(使用条件变量和互斥锁实现)
两种角色 :生产者(pro 线程) 消费者(con 线程)
一个场所: 阻塞队列(BlockQueue)
在一个时间点只有一个消费者或者生产者可以进行数据提取/数据传输(本质:只有一把锁)
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include <time.h>
namespace my_blockq
{
template<class T>
class BlockQueue{
public:
BlockQueue(int capacity):_capacity(capacity){
pthread_mutex_init(&_mux, nullptr);
pthread_cond_init(&_full, nullptr);
pthread_cond_init(&_empty, nullptr);
}
~BlockQueue(){
pthread_mutex_destroy(&_mux);
pthread_cond_destroy(&_full);
pthread_cond_destroy(&_empty);
}
public:
void LockQueue(){
pthread_mutex_lock(&_mux);
}
void UnlockQueue(){
pthread_mutex_unlock(&_mux);
}
void producter_wait(){
//首先会释放锁,然后将所在线程挂起,返回时会主动竞争锁,获取到锁后,才能返回
pthread_cond_wait(&_full, &_mux);
}
void consumer_wait(){
pthread_cond_wait(&_empty, &_mux);
}
void consumer_wakeup(){
pthread_cond_signal(&_empty);
}
void producter_wakeup(){
pthread_cond_signal(&_full);
}
//生产数据
void Push(const T& in){
//向队列中放入数据
LockQueue(); //因为即将访问临界资源所以需要上锁
while (is_full()){ //while循环防止线程被伪唤醒,或者prothread_cond_wait函数异常等引起的情况,保证当队列满的时候,生产者进程必须在这里挂起,无法进入后续代码
producter_wait(); //如果阻塞队列满了,需要等待消费者消费数据,将生产者进程挂起
}
_bq.push(in); //向阻塞队列中插入数据
consumer_wakeup(); //此时阻塞队列中有数据了,可以去将消费者进程唤醒消费
UnlockQueue(); //解锁
}
//消费数据
void Pop(T* out){
//向队列中读取数据
LockQueue();
while (is_empty()){
consumer_wait();
}
*out = _bq.front();
_bq.pop();
producter_wakeup();
UnlockQueue();
}
private:
bool is_full(){
return _bq.size() == _capacity;
}
bool is_empty(){
return _bq.empty();
}
private:
std::queue<T> _bq; //队列
int _capacity; //容量
pthread_mutex_t _mux; //保护临界资源的锁
//1.当生产满了就不要生产了(不要竞争锁了),应该让消费者进行消费
//2.当消费空了,就不要再消费了(不要再竞争锁了), 应该让生产者生产
pthread_cond_t _full; //bq满了
pthread_cond_t _empty; //bq空了
};
}
1.int类型的值作为生产消费的对象
#include "BlockQueue.h"
#include <unistd.h>
using namespace my_blockq;
void* con_run(void* args){
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while (true){
int data = 0;
bq->Pop(&data);
std::cout << "消费者消费了一个数据 " << data << std::endl;
sleep(1);
}
}
void* pro_run(void* args){
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while (true){
//使用随机数创建交互数据
int data = rand() % 30 + 1;
bq->Push(data);
std::cout<< "生产者生产了一个数据 " << data << std::endl;
}
}
int main(){
srand((long long)time(nullptr));
BlockQueue<int> *bq = new BlockQueue<int>(5); //阻塞队列,交易场所,临界资源
pthread_t con, pro;
pthread_create(&con, nullptr, con_run, (void*)bq); //消费者执行流
pthread_create(&pro, nullptr, pro_run, (void*)bq); //生产者执行流
//对两个执行流进行等待接收
pthread_join(con, nullptr);
pthread_join(pro, nullptr);
return 0;
}
2.task类型的任务作为生产消费的对象
#include "BlockQueue.h"
#include <unistd.h>
using namespace my_blockq;
struct task{
int _a;
int _b;
char _op;
task(int a, int b, char op)
:_a(a), _b(b), _op(op){}
int operator()(){
int ret = 0;
switch(_op){
case '+':
ret = _a + _b;
break;
case '-':
ret = _a - _b;
break;
case '*':
ret = _a * _b;
break;
case '/':
ret = _a / _b;
break;
default:
std::cout << "task failed" << std:: endl;
}
return ret;
}
};
const char* ch_arr = "+-*/";
void* con_run(void* args){
BlockQueue<task>* bq = (BlockQueue<task>*)args;
while (true){
task data(0, 0, 0);
bq->Pop(&data);
int end = data();
std::cout << "消费者[" << pthread_self() <<"]执行任务" << data._a << data._op << data._b << "=" << end << std::endl;
//std::cout<< "消费者[ " << pthread_self() << " ]消费了一个数据 " << data << std::endl;
sleep(1);
}
}
void* pro_run(void* args){
BlockQueue<task>* bq = (BlockQueue<task>*)args;
while (true){
int a = rand() % 30 + 1;
int b = rand() % 30 + 1;
char op = ch_arr[rand() % 4];
task data(a, b, op);
bq->Push(data);
std::cout << "生产者[" << pthread_self() <<"]派发任务" << data._a << data._op << data._b << "=?" << std::endl;
//std::cout<< "生产者[ " << pthread_self() << " ]生产了一个数据 " << data << std::endl;
sleep(1);
}
}
int main(){
srand((long long)time(nullptr));
BlockQueue<task> *bq = new BlockQueue<task>(5);
pthread_t con, pro;
pthread_t con2, pro2;
pthread_t con3, pro3;
pthread_create(&con, nullptr, con_run, (void*)bq);
pthread_create(&con2, nullptr, con_run, (void*)bq);
pthread_create(&con3, nullptr, con_run, (void*)bq);
pthread_create(&pro, nullptr, pro_run, (void*)bq);
pthread_create(&pro2, nullptr, pro_run, (void*)bq);
pthread_create(&pro3, nullptr, pro_run, (void*)bq);
pthread_join(con, nullptr);
pthread_join(pro, nullptr);
}
生产和消费,传输数据只是其中的一步,我们还应该考虑数据是怎么来的??数据该怎么处理??
现阶段我们的生产消费模型使用的是随机数产生数据,进行简单的加减任务,显然耗时比较少。看起来主要时间花在数据传输,并且生产者,消费者还需要互相等待消耗效率,显得优点鸡肋,好像和单线程函数调用相似。但是获得数据和处理数据时间增多的时候,生产消费者模型就非常有意义了,数据传输的消耗较低,但是多个CPU可以同时处理多个线程,使多个线程同时获取数据和多个线程同时数据处理,提高效率,生产消费同时进行,两者解耦
使用条件变量,使生产消费具有一定的顺序性,生产快的时候让生产者挂起,等待消费者消费,当生产慢的时候,让消费者挂起,等待生产者生产,实现生产消费的同步进行,因为只有一把锁所以只有一个线程可以访问临界资源
7.0 POSIX信号量
7.1 信号量的基本概念
信号量本质就是一把计数器,描述临界资源中的资源数目大小 ,用于预定资源(最多能有多少资源分配给线程)
临界资源如果可以被划分成一个一个小资源,如果处理得当,可以让多个线程访问临界资源的不同区域,实现并发
每个线程想要访问临界资源, 都要先申请信号量资源
信号量模拟实现(伪代码)
信号量这个类型中应该封装了一把锁,来保证信号量的线程安全
7.2 信号量相关接口介绍
sem_t 信号量类型
1.sem_init(初始化)
第一个参数 sem
信号量指针,告诉函数要初始化哪一个信号量
第二个参数 pshared
填0标识线程间共享,填非零代表进程间共享
第三个参数 value
信号量初识大小,代表资源有多少份
2.sem_destroy(销毁/释放)
3.sem_wait(++)
代表一份资源已经被预定,信号量数值减减
4.sem_post(- -)
在生产消费模型中代表一份资源被生产出来,信号量数值++
7.3 基于环形队列的生产消费模型实现
环形队列如何判空,满??
1.计数器:放一个元素计数器++,拿一个计数器–
2.镂空:当放的下标++后等于拿的下标,则不能放入,需要等待拿
我们可以使用数组来模拟实现环形队列,只要将 index % _capacity就可以实现环形队列了
生产者消费者刚开始指向同一个位置 【队列为空】,生产者消费者在队列为满的时候也指向同一个位置
队列不为空,也不为满的时候,生产者和消费者一定指向的不是同一个位置,这种情况下生产和消费就可以并发进行。
当生产消费在同一个位置上,队列满就要让消费者先执行,队列空就要让生产者先执行(同步),并且此时生产消费必须互斥
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
namespace ns_ring_queue
{
const int g_cap_default = 10;
template <class T>
class RingQueue
{
private:
int _capacity;
std::vector<T> _ring_queue;
//空格(没有数据的格子) 的信号量
sem_t _space_sem;
//资源(有数据的格子) 的信号量
sem_t _data_sem;
int _c_step;
int _p_step;
pthread_mutex_t _con_mtx;//互斥锁,实现消费者线程间互斥
pthread_mutex_t _pro_mtx;//互斥锁,实现生产者线程间互斥
public:
RingQueue(int cap = g_cap_default) : _capacity(cap)
{
_c_step = 0; //消费者的初始index
_p_step = 0; //生产者的初始index
_ring_queue.resize(_capacity); //初始化环形队列,先开辟得到_capacity 个合法空间
pthread_mutex_init(&_con_mtx, nullptr);//初始化锁
pthread_mutex_init(&_pro_mtx, nullptr);
sem_init(&_space_sem, 0, 10);//空格子开始有10个
sem_init(&_data_sem, 0, 0); //有资源的格子开始有0个
};
~RingQueue()
{
sem_destroy(&_data_sem); //销毁信号量和锁
sem_destroy(&_space_sem);
pthread_mutex_destroy(&con_mtx);
pthread_mutex_destroy(&pro_mtx);
}
public:
void Push(const T& in)
{
sem_wait(&_space_sem); //让所有线程开始预定资源(抢夺信号量)
pthread_mutex_lock(&_pro_mtx); //需要访问临界资源进行上锁
_ring_queue[_p_step] = in; //插入数据
_p_step++;
_p_step = _p_step % _capacity; //生产者步数向后走一步
sem_post(&_data_sem); //有资源的格子数量++,将_data_sem信号量++
pthread_mutex_unlock(&_pro_mtx);
}
//当队列满时,_space_sem信号量为0,生产者不能生产,将被挂起等待消费者消费
void Pop(T *out)
{
sem_wait(&_data_sem);
pthread_mutx_lock(&_con_mtx);
*out = _ring_queue[_c_step];
_c_step++;
_c_step %= _capacity;
sem_post(&_space_sem);
pthread_mutex_unlock(&_con_mtx);
}
//当队列空时,_data_sem信号量为零,消费者不能消费,将被挂起等待生产者生产
};
}
#include "ring_queue.h"
using namespace ns_ring_queue;
struct task
{
int _a;
int _b;
char _op;
task(int a, int b, char op)
: _a(a), _b(b), _op(op) {}
int operator()()
{
int ret = 0;
switch (_op)
{
case '+':
ret = _a + _b;
break;
case '-':
ret = _a - _b;
break;
case '*':
ret = _a * _b;
break;
case '/':
ret = _a / _b;
break;
default:
std::cout << "task failed" << std::endl;
}
return ret;
}
};
const char *ch_arr = "+-*/";
void *con_run(void *args)
{
RingQueue<task> *rq = (RingQueue<task> *)args;
while (true)
{
task data(0, 0, 0);
rq->Pop(&data);
int end = data();
std::cout << "消费者[" << pthread_self() <<"]执行任务" << data._a << data._op << data._b << "=" << end << std::endl;
//std::cout << "消费了一个数据 " << data << std::endl;
}
}
void *pro_run(void *args)
{
RingQueue<task> *rq = (RingQueue<task> *)args;
while (true)
{
int a = rand() % 20 + 1;
int b = rand() % 20 + 1;
int op = ch_arr[rand() % 4 ];
task data(a, b, op);
rq->Push(data);
std::cout << "生产者[" << pthread_self() <<"]派发任务" << data._a << data._op << data._b << "=?" << std::endl;
//std::cout << "生产了一个数据" << data << std::endl;
sleep(1);
}
}
int main()
{
srand((long long)time(nullptr));
pthread_t con, pro;
pthread_t con2, pro2;
pthread_t con3, pro3;
RingQueue<int> *rq = new RingQueue<int>(10);
pthread_create(&con, nullptr, con_run, (void *)rq);
pthread_create(&con2, nullptr, con_run, (void *)rq);
pthread_create(&con3, nullptr, con_run, (void *)rq);
pthread_create(&pro, nullptr, pro_run, (void *)rq);
pthread_create(&pro2, nullptr, pro_run, (void *)rq);
pthread_create(&pro3, nullptr, pro_run, (void *)rq);
pthread_join(con, nullptr);
pthread_join(con2, nullptr);
pthread_join(con3, nullptr);
pthread_join(pro, nullptr);
pthread_join(pro2, nullptr);
pthread_join(pro3, nullptr);
return 0;
}
实验现象:
8.0 线程安全vs可重入
8.1 线程安全vs可重入
可重入和线程安全概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,
并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们
称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重
入函数,否则,是不可重入函数
可重入和线程安全的联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入和线程安全的区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生
死锁,因此是不可重入的
8.1 STL 线程安全
STL不是线程安全的
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
8.2 智能指针线程安全
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这
个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数
9.0 线程池
线程池基础概念:一种线程使用模式,提高任务处理效率。线程过多会带来调度开销,进而影响缓存局部性和整体性能。线程池内置多个线程,等待管理者分配可并发执行的任务。避免了在处理短时间任务时避免创建销毁线程的代价。线程池不仅能够保证内核的充分利用,还可以防止过分调度。可用线程数量取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景
1.需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使
用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于
长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大
多了。
2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没
有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程
可能使内存到达极限,出现错误.
9.1 线程安全的单例模式
单例模式是一种非常经典的设计模式。某些类, 只应该具有一个对象(实例), 就称之为单例.在很多服务器开发场景中, 某些类型非常的大,经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据,防止相同数据重复拷贝
单例模式的实现方式: 饿汉模式和懒汉模式
饿汉模式:通过一个类来包装单例对象,则一个进程就只能有有一个单例对象
template<typenameT>
class Singleton{
static T data; //静态成员变量(单例对象)
public:
static T* GetInstance() { // 获取单例对象的地址
return &data
}
};
将单例对象作为一个静态成员变量封装到Singleton函数中,静态成员类外初始化后就可以通过Singleton的类名调用对象以实现单例模式
懒汉模式:延时加载,优化服务器的启动速度
//非线程安全
template<typenameT>
class Singleton{
static T* inst; //静态成员变量(单例对象)
public:
static T* GetInstance() { // 若对象不存在则按创建单例对象,若已经存在则直接返回对象地址
if (inst == nullptr){
inst = new T();
}
return inst;
}
};
//线程安全型
当我们想要调用单例对象的时候,直接调用GetInstance()函数,若单例对象还未创建则进行创建,若已经创建则返回已存在对象的地址,并且我们可以将这个类的构造函数设置成private,拷贝构造,赋值运算符重载函数进行delete,保证只能通过Getinstance()来获取单例。
注意:懒汉模式需要考虑线程安全问题,因为若两个线程同时调用Getinstance()函数可能会同时构建两个单例对象,在接下来的线程池编写中我们会采用线程安全的模式来实现
9.2 线程池的工作原理
在这个过程中,任务队列就是临界资源,不管是向内部Push任务还是Pop任务都需要对临界资源进行保护,否则就可能出现一个位置有两个任务同时Push或者Pop的现象,所以线程池的工作原理就是我们的生产消费模型。
我们通过提早创建线程,避免了接到任务才去创建后销毁线程的不必要开销,提高了效率。并且使得生产任务和处理任务进行解耦
9.3 线程池的实现
线程池框架
#define THREAD_NUM 6
class ThreadPool
{
private:
//将构造函数私有化
ThreadPool(int num = THREAD_NUM) : _pthread_num(num), _stop(false)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
public:
~ThreadPool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
static void *ThreadRoutine(void *args); //线程池中每一个线程跑的代码
static ThreadPool *GetInstance(); //获取线程池单例
bool InitThreadPool(); //初始化线程池
void PushTask(const Task &task); //放入任务
void PopTask(Task &task); //拿出任务
void ThreadWait() { pthread_cond_wait(&_cond, &_lock); } //以下为锁和条件变量的基本使用
void ThreadWakeUp() { pthread_cond_signal(&_cond); }
void ThreadLock() { pthread_mutex_lock(&_lock); };
void ThreadUnlock() { pthread_mutex_unlock(&_lock); };
bool IsStop() { return _stop; } //
bool TaskQueueIsEmpty() { return _task_queue.empty(); } //判断任务队列是否为空
private:
std::queue<Task> _task_queue; //任务队列
size_t _pthread_num; //线程池中具有的线程数量
bool _stop;
pthread_mutex_t _lock; //多线程互斥锁
pthread_cond_t _cond; //多线程条件变量
static ThreadPool *_single_instance; //线程池获取单例的指针
};
ThreadPool *ThreadPool::_single_instance = nullptr; //初始化获取单例的指针
线程池成员函数具体实现
1. GetInstance() : 获取单例
//线程池成员函数具体实现
ThreadPool *ThreadPool::GetInstance()
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //使用的是全局或者静态的锁,可以直接用宏创建并且初始化
if (_single_instance == nullptr){ //使用双重if语句,降低锁冲突概率,提高性能
pthread_mutex_lock(&mutex); //走到这说明单例并未创建,则使用互斥锁,保证只有一个线程可以创建单例
if (_single_instance == nullptr) //判断单例对象是否已经存在 //无需手动初始化或者销毁
{
_single_instance = new ThreadPool(); //因为GetInsrance函数在类内部,所以可以调用成员函数(构造函数)
_single_instance->InitThreadPool(); //对线程池进行初始化
pthread_mutex_unlock(&mutex); //解锁
}
}
return _single_instance; //返回单例地址
}
1.使用线程互斥锁来保证多线程安全,只有第一个拿到锁的线程才可以判断单例未被创建进行创建单例,保证不会有两个线程同时进入if语句创建两次单例。其他线程再次拿到锁的时候单例已经被创建,返回单例的地址
2.使用双重 if 语句进行判空,当单例已经创建情况下,多个线程可以同时直接进行语句判断若将最外层 if 去掉,则线程想要到达判断语句则必须上锁,大大降低了效率,双重 if 判空可以降低锁冲突的概率,提高性能,也可以保证线程安全
3.我们还可以给_single_instance 加上volatile关键字,防止编译器优化导致错误
2.InitThreadPool() : 创建_pthread_num个线程执行 ThreadRoutine函数
bool ThreadPool::InitThreadPool()
{
pthread_t tid;
for (size_t i = 0; i < _pthread_num; i++)
{
int ret = pthread_create(&tid, nullptr, ThreadRoutine, (void *)this); //注意这里传入了this指针
if (ret != 0)
{
LOG(FATAL, "Create ThreadPool error");
return false;
}
}
LOG(INFO, "Create ThreadPool Success");
return true;
}
ThreadRoutine是一个参数未void*, 返回值也是void* 类型的函数,所以其不能直接定义成线程池的普通成员函数,而是定义成静态成员函数,这是因为普通成员函数在传参过程中会自动传入this指针,导致于pthread_create()函数中的函数指针参数不匹配导致报错。所以我们采用静态成员函数,并通过pthread_create的第四个参数来传入this指针,保证线程池中的各个线程可以调用线程池的各种函数
3.ThreadRoutine : 线程池中的线程跑的代码
void *ThreadPool::ThreadRoutine(void *args)
{
ThreadPool *tp = (ThreadPool *)args; //接收this指针
while (true) //不断执行任务
{
Task task(); //创建task对象,等待接收任务
tp->ThreadLock(); //进行加锁
while (tp->TaskQueueIsEmpty()) //若任务对垒未空,则让线程进入等待阶段。为防止ThreadWait等函数出错。采用while方法
{
tp->ThreadWait();
}
tp->PopTask(task); //获取任务
tp->ThreadUnlock(); //解锁
task.ProcessOn(); //处理任务
}
}
可以看到拿取任务需要上锁,者处理任务处于分离状态,代表一个任务拿到任务后便可以自行去处理,不会受锁的影响。假如使用的是一台多个CPU的机器,则多个线程可以在多个CPU上同时处理任务,大大提高了效率,只有拿任务这个动作是需要互斥保护,实现了生产消费,异界分配消费之间的解耦
PushTask 和 PopTask : 放入/拿出 任务
void ThreadPool::PushTask(const Task &task)
{
ThreadLock();
_task_queue.push(task);
ThreadWakeUp();
ThreadUnlock();
}
void ThreadPool::PopTask(Task &task)
{
task = _task_queue.front();
_task_queue.pop();
}
放入任务的动作是线程池以外的线程做的所以需要在PushTask函数中进行上锁,而PopTask是我们线程池中的线程做的,在此之前还需要对任务队列进行判空等操作,所以需要在线程执行的代码中上锁,当执行到TaskPop函数的时候,线程其实已经是上锁状态
10.读者写者模型
读者写者模型适用场景:
1.对数据大部分操作是读取,少部分操作是写入
2.进行数据读取的一端,并不会把数据取走
10.1读者写者模型基本操作
本质:使用读写锁来维护两种角色之间的三种关系,对于读者写者模型pthread库提供了专门的加锁接口,使用方法和互斥锁相似
1.pthrea_rwlock_inIt (锁的初始化)
第一个参数 rwlock
pthread_rwlock_t 类型的指针,pthread_rwlock_t 明显就是一个读者写者模型的互斥锁,传入指针可以完成初始化
第二个参数 attr
设置锁的属性,传入NULL, 操作系统会自动设置成默认属性
2.pthread_rwlock_destroy(销毁锁)
3.pthread_rwlock_rdlock (以读者身份加锁)
阻塞式等待,若没有得到锁则暂停程序等待获取锁
4.pthread_rwlock_trylock (以读者身份加锁)
非阻塞式等待,0示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待
3.pthread_rwlock_rdlock (以写者身份加锁)
阻塞式等待,若没有得到锁则暂停程序等待获取锁
4.pthread_rwlock_trylock (以写者身份加锁)
非阻塞式等待,0示成功,非0表示错误码,非阻塞会返回ebusy而不会让线程等待
pthread_rwlock_unlock(释放读写锁)
10.2 读者写者模型原理
在读写锁中存在一个 int 类型的 readers变量,每当有一个读者进入以读者身份上锁,对此变量++,然后读者才可以进行读取操作。若读取完毕则以读者身份再次加锁对readers–,这样就可以保证读者在读取数据时,锁并不在自己身上,其他线程可以拿到锁进行数据读取。
而在有读者在读的情况下,写者不能进行数据写入,所以必须让写者进行等待。因为写者数量并不多(读写模型中大部分操作为读取),所以可以先加锁,若读者全部退出时,放可以进行数据写入。写完后解锁允许下一个写者进行操作
写者 读者
mutex.lock(); mutex.lock();
while (readers > 0){ readers++;
Wait(); mutex.unlock();
} //进行读取操作
//对数据进行操作 mutex.lock();
mutex.unlock(); readers--;
mutex.unlock()
在初始化锁的时候我们可以设置锁的属性,这个属性可以控制读者写者的优先级。即读者写者同时到来时,到底锁先给读者还是写者
我们还可以通过pthread_rwlockattr_setkind_np函数进行设置,这里就不展开赘述了