当前位置: 首页 > news >正文

Linux——多线程

目录

一背景知识

aIO的基本单位

b真实的页表

c代码数据划分  

二线程

1概念

2不同系统的线程

3CPU眼中的线程

4相关代码操作

5问题

6线程优点

 7线程缺点

8进程VS线程

​三线程控制

1pthread库

​编辑 2相关函数和八大问题

3tid

4__thread 

四线程封装

五线程互斥 

1多线程访问

2认识锁与对应接口

3锁的种类

1全局锁

 2局部锁

 4理解锁

5实现角度理解锁

6可重入VS线程安全

7死锁  

7.1必要条件

7.2避免死锁

 六线程同步

1条件变量 

a.快速地认识接口

b.认识条件变量

c.测试代码

七生产消费模型

1认识生产消费模型

​编辑 2实现生产消费模型

3读写锁

 3.1理解读写者问题

 3.2测试代码

4自旋锁 

4.1原理

4.2测试代码 

 4.3使用场景

八信号量 

1相关接口 

2实现

九线程池

十日志


一背景知识

aIO的基本单位

输出结论:OS进行内存管理,不是以字节为单位的:而是以内存块为单位的,大小是:4KB

如果你学了文件系统,是不是对这个感到很熟悉:系统与磁盘文件进行IO的基本单位也是:

4KB(8个扇区)

在磁盘中的每个文件都要有自己的inode号,数据内容与属性;而数据内容是以4KB数据块大小的形式进行保存;当磁盘中的文件要加载到内存时(IO交互),就以数据块(页帧)加载到对应的一块一块的4KB内存空间(页框)中,进行后续的数据处理

(在32位下)如果用计算器来计算页框总个数:共有1,048,576个页框

怎么多的页框OS要怎么进行管理呢?

六字正言:先描述,在组织

用struct page结构体中描述页框的基本属性:int falg; //是否被占用  int mode; //页框的权限..  在用struct page memory[1048576]数组,结合OS调度算法进行对内存的管理!

那页框用结构体来表示,总大小会不会很大呢:1个页框(用结构体表示大小10字节)

1百万多个总共才10MB多<_<

而这样就使每块页框都有了下标,下标+4KB不就找到了每个页框的起始地址了吗!!

所以:OS内部,内存时不需要用格子来明确每个页框(只是方便我们进行学习来用的格子)

b真实的页表

之前为了方便,讲页表时说:页表的作用是虚拟地址转成物理地址;但这样还是太理解它、;

现在:我们来谈谈页表到底干了什么事?

(以32位为例)虚拟地址共有32bit;其中:

前10个bit索引页目录当中的某一项(0到1023):页目录里面存着(二级)页表的地址

后10个bit索引页表当中的某一项(0到1023):页表里面存着页框的起始地址

最后12个bit表示页框号:通过页表存的页框的起始地址 + 页框号 = 物理地址

想直接找到页框号:虚拟地址&(0xFFF)就找到了页框号

总共大小:(页目录)1024*4字节 +(二级页表)1024*1024*4字节 =4MB + 4KB

(页目录有1024项,二级页表就有多少张;但并不是都会创建出来:按需创建)

这只是最坏情况:真实情况  <= 4MB + 4KB

这样的设计,大大节省了空间

页框起始地址通过虚拟地址的后12bit进行偏移量:就能定位到页框的每个字节啦

但如果要访问的是8字节,几十字节的结构体呢?是不是会出现问题??

根本不用担心!!C/C++给我们提供了类型这一语法概念;我们在取地址时虽然只拿到一个数字;但在汇编层面上要告诉CPU类型是什么

当我们对变量进行操作时,(如果是int类型)读取时会一次访问4字节的空间从而进行处理!!

上面大致讲完了虚拟到物理地址的转化:但能转化的前提:找到页目录,怎么找到的??

在前面的信号部分中:在写到野指针时我们说CPU内的cr2:页故障线性地址寄存器,帮助我们判断读到的虚拟地址是否有异常:如果有:状态标记位由0变1并告诉OS进程出现异常;

cr3寄存器:储存着进程页目录起始地址,MMU根据cr3进行寻址过程(虚拟转物理过程)

所以从CPU出现的都是物理地址了(三位一体)

c代码数据划分  

在地址空间中,我们所说的:正文代码区,初始化数据区,未初始化数据区...进行划分的意义:

为了限定一批虚拟地址的范围(这个范围CPU必须依靠页表才能看到)

以正文代码区为例:里面的20个函数要进行划分给不同的进程(考虑单进程),在技术上有没有可行性?

要回答这个问题,首先要明白:

函数有地址吗?

有:函数名表示函数的入口(起始)地址:每行代码都是地址,我们认为地址是连续的;

函数是什么?

函数是由连续的地址构成的代码块,即:一个函数——对应一批连续的虚拟地址!!

我们在对20个函数划分给不同的进程,其实是在将页表(虚拟地址)进行拆分!!

代码数据的划分过程,其实也就表明:虚拟地址是一种资源!!

二线程

1概念

线程:在进程内部运行,是CPU调度的基本单位

之前我们在创建子进程时,要把父进程中的task_struct,地址空间,页表...都要拷贝一份给子进程;

但如果要是要创建线程的话,就只需创建task_struct,让task_struct共享进程的数据!! 

但这与我们之前所学的进程之前存在什么联系呢?或者说要如何对线程进行理解?

学习进程时,我们说:进程 = 内核数据结构 +进程代码和数据

但在现在我们要往深入去理解线程:OS内核看待进程:进程是承担分配资源的实体

这里引用一个小故事来理解它:

在我们伟大的中国,承担分配社会资源的实体是什么?是一个个家庭;

在这个家中,你在执行读书的代码,你的父母执行赚钱的代码,你的爷爷奶奶执行养老的代码;

你们之间是互不干扰的:但你们都有一个共同的任务(目标):把日子过好;为了这个任务:家庭中的成员只要好好地执行好自己手里面的事情,这个任务也就达成了!!

也就是说:不同的线程也许在做着相同或不同的事:但都在为了某个任务能够完成而‘工作’

对比以前学习的进程:

以前学习时,进程内部只有一个执行流(一个task_struct),而现在讲的线程无非就是多了几个执行流而已;

或者说:进程内部多了几个PCB共享着同一份地址空间,页表...就变成了线程

2不同系统的线程

OS要单独设计线程:新建?暂停?销毁?调用?线程是否要与进程产生关联?

设计后如何管理线程?—— 先描述,在组织

(线程)struct tcb {     //线程的id,优先级,状态,上下文,恢复...       } 

在(进程)struct pcb中包含tcb(线程)(以类似链表的形式进行关联起来)

如果这么做:线程与进程就没有联系了,互不干扰;进行维护的调用算法就要有两套

而这些是Window内部实现线程方式:提供了真实的线程控制块

但Linux的设计者想:为什么我要单独设计出一个数据结构来表示线程呢?

他发现线程被调度本质上是执行流,而进程也是如此:

于是复用PCB,用进程来实现线程,就不用再单独设计出一套数据结构和调度算法了;

在实际情况下,Linux在出现问题与解决问题上也就比Window更不容易出错更快进行修复,上手也比Window更简单,更优雅!!

3CPU眼中的线程

以前执行进程时,CPU只看到一个执行流;现在执行线程时:

在CPU看来:task_struct的个数<= 进程:CPU对task_struct要不要区分是进程还是线程?

不做区分:是进程还是线程,反正都是执行流,拿过来你的task_struct,地址空间,页表...我来帮你执行就行了,哪用得着这么多废话;

所以:CPU看到的执行流 <= 进程,也就是:

Linux执行的执行流是:轻量级进程  

来到这,我们也就能理解:线程(执行流)是CPU调度的基本单位了

4相关代码操作

说再多不如见一见:

#include <iostream>
#include <unistd.h>
#include <ctime>// 新线程
void *threadStart(void *args)
{while (true){sleep(1);std::cout << "new thread running..." << ", pid: " << getpid() << std::endl;}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadStart, (void *)"thread-new");// 主线程while (true){std::cout << "main thread running..." << ", pid: " << getpid()<< std::endl;sleep(1);}return 0;
}

现象:用:ps ajx | grep 查进程时,只有一个进程且主新线程的pid是一样的

而用:ps -aL 查线程时,会发现它们的LWP(Light Weight Process(轻量化进程))是不同的,即:CPU在调度时,看的是:LWP

这么说的话:之前的多进程,单进程调度会有影响吗?

不影响!多进程与单进程调度,LWP=PID是一样的!!

注:进行编译时:要链接pthread:-lpthread库才能编译成功! 否则会报错!

5问题

a已经有多进程了,为什么还要有多线程??

a.启动时:线程创建成本低(进程创建是要创建地址空间,页表,pcb...而线程创建只要有pcb)

b.运行时:线程调度成本低(进程调度要切换上下文,页表,地址空间...而线程只要保留上下文)

这样说还够,还要结合硬件来谈:CPU内的cache储存器

进程被调度时,cache里会保留进程相关热数据;如果CPU切换进程时,CPU先在cache里找该进程的热数据,找不到就要才重新加载该进程的数据:这个过程就非常的浪费时间;而线程调度时,cache的热数据是进程内的所有线程所共享的(下面证明),切换线程不用再重新加载数据,极大提高效率;这才是线程调度成本低的根本原因!!

c.删除也是同样的道理:线程只要直接干掉pcb就好了

但为什么还要有进程?

本文主要讲的是线程,当然要好好地吹一波:但也要明白它的缺点:

一个线程异常,整个进程全部终止;进程与线程两者具有不可替代性

b不同系统对应进程和线程的实现不一样:为什么OS的课本只有‘一本’?

这就好比:在抗日战争时:你的连长说:今天我们要对敌人展开歼灭战;你要枪干掉敌人或者是你的队友与敌人肉搏干掉敌人,两者的做法虽然不同,但都是在完成连长的目标

同样,虽然不同的系统在对线程的实现不同:Linux用进程来模拟线程;Windows单独设计出线程tcb来实现线程...但都是在遵守着OS的原则:线程在进程内部运行,是CPU调度的基本单位;操作系统这门学科就相当于计算机界的‘马哲’;虽然生涩难懂,但处处不在!

c线程运行
int main()也是一个函数入口,主要执行者是主线程;创建新线程时需要用户自己去设计出一个函数让新线程执行:也就是这个线程负责执行这块代码块,另一个线程执行这块代码块...虽然在物理上线程之间是共享代码的,但从逻辑上线程之间也可以说是独立的

前面说了:虚拟地址本质上是一种资源;而线程就是对资源来进行分配与使用!

6线程优点

线程的优点作总结:

a.创建一个新线程的代价要比创建一个新进程得多
b.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要很多
c.线程占用资源要比进程很多
d.能充分利用多处理器的可并行数量(也是进程的优点)

e.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务(也是进程的优点)
 f.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

(在进行4G数据作加密解密:如果CPU是4核的,可创建4个线程去并发执行)
g.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

(上传下载4G文件时,可将文件分为4个1G文件,让4个线程去执行)

 7线程缺点

1健壮性降低:一个线程出错,整个进程都崩溃!

#include <iostream>
#include <unistd.h>
#include <ctime>// 新线程
void *threadStart(void *args)
{while (true){sleep(1);int x = rand() % 5;std::cout << "new thread running..." << ", pid: " << getpid() << "x:" << x << std::endl;if (x == 0){int *p = nullptr;*p = 10; // 野指针}}
}int main()
{srand(time(nullptr));// 创建三个线程pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");pthread_t tid2;pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");pthread_t tid3;pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");// 主线程while (true){std::cout << "main thread running..." << ", pid: " << getpid() << std::endl;sleep(1);}return 0;
}

现象: 一个线程崩溃影响整个进程

2缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

比如:主线程修改了全局变量,线程看到的全局变量是被修改后的值(线程之间大部分资源共享)

#include <iostream>
#include <unistd.h>
#include <ctime>int gval=100;
// 新线程
void *threadStart(void *args)
{while (true){sleep(1);std::cout << "new thread running..." << ", pid: " << getpid()<<",gval:"<<gval<<",&gval:"<<&gval<<std::endl; }
}int main()
{srand(time(nullptr));// 创建三个线程pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");pthread_t tid2;pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");pthread_t tid3;pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");// 主线程while (true){std::cout << "main thread running..." << ", pid: " << getpid() <<",gval:"<<gval++<<",&gval:"<<&gval<<std::endl;sleep(1);}return 0;
}

现象:

3编程难度提高:编写与调试一个多线程程序比单线程程序困难得多(这个因人而异)

8进程VS线程

1概念上:

进程资源分配的基本单位;线程调度的基本单位

2资源分配上:

进程的多个线程共享以下进程资源和环境:

a地址空间(全局变量,函数调用...)
b文件描述符表(打印数据在同一个显示器文件上...)
c每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
d当前工作目录
e用户id和组id

但线程也拥有着自己的一部分数据:
a线程ID
b一组寄存器硬件上下文数据,反映出线程可以动态运行
c线程在运行时,会形成各种临时变量,临时变量会被每个线程保存在自己的栈区中
derrno
e信号屏蔽字
f调度优先级

进程与线程的关系图如下:

 三线程控制

1pthread库

故事时间:

你是一个大学生;在学校里学了OS中的创建线程,终止线程,调度线程,等待线程...

而今天你打算在Linux系统环境中去写一些代码来巩固所学的线程;

Linux系统中有线程吗?没有!只有轻量级进程(模拟线程);

但你不管,只认线程;这就要求提供线程相关的系统调用:给上层用户提供创建线程的接口;但这不就是让程序员来担任产品经理吗?(引发众怒!)

所以为了解决问题引发的问题:

就有了pthread库:

Linux自带的原生线程库:对轻量级进程接口进行封装,按照线程的方式交给用户;

也因此:Linux中的实现的线程也称为:用户级线程

而Windows实现的线程(真实存在)称为:内核级线程

 2相关函数和八大问题

简单进行使用:

#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
using namespace std;void* ThreadRun(void*args)
{int cnt=5;while(cnt){cout<<"New Thread Running..."<<"cnt:"<<cnt--<<endl;sleep(1);}
}int main()
{pthread_t pid;int n = pthread_create(&pid,nullptr,ThreadRun,(void*)"NewThread");if(n!=0){cerr<<"Create New Thread fail..."<<endl;}//阻塞等待新线程->不等行不行?int m=pthread_join(pid,nullptr);if(m==0){cout<<"Main Thread Wait Sucess..."<<endl;}}

现象:

问题1:main线程与new 线程谁先运行?- > 不确定

问题2:我们期望谁最后退出? - > 主线程; 你如何保证? ->join保证(主线程阻塞等待)

问题3:tid是什么样子的呢?是什么?

下面我们来打印出来看看:

void PrintTid(pthread_t pid)
{char buffer[64];snprintf(buffer, sizeof buffer, "0x%lx", pid);cout << "pid:" << buffer << endl;
}

pid是一个地址:虚拟地址为什么?后面再说~ 

 问题4:全面看待线程函数传参:void*arg

试着把name传进去: 

void *ThreadRun(void *name)
{string ThreadName = *((string *)name);int cnt = 5;while (cnt){cout << ThreadName << " Running..." << "cnt:" << cnt-- << endl;sleep(1);}
}int main()
{pthread_t pid;string name = "NewThread 1";int n = pthread_create(&pid, nullptr, ThreadRun, (void *)&name);...
}

但学了C++之后,我们要知道也可以继续传对象进去:

class Thread
{
public:string name;int num;
};
void *ThreadRun(void *td)
{Thread *Td = static_cast<Thread *>(td);int cnt = 5;while (cnt){cout << Td->name << " Running..." << "num:" << Td->num << " cnt:" << cnt-- << endl;sleep(1);}delete Td;return nullptr;
}int main()
{pthread_t pid;Thread td;td.name = "NewThread";td.num = 1;int n = pthread_create(&pid, nullptr, ThreadRun, (void *)&td);...
}

总结:我们可以传递任意类型:但你一定要能想得起来,也可以传递类对象的地址!!!

但我们非常不推荐直接在main函数里创建对象:(为什么?)

1. 我们之前说了每个线程都要有独立的栈结构来处理临时变量;你往main函数中创建对象不就破坏了主线程的独立性(完整性)吗!

2.如果再来一个线程,也用着同样的对象;

使用的同时还把对象修改了:前一个线程传进来的对象也同时被修改了!

所以我们推荐new 对象(在堆上申请空间)

问题5: 全面看待线程函数返回:void**returnval

尝试用上面的代码进行改造,让主线程获取新线程的返回值:

class Thread
{
public:string name;int num;
};
void *ThreadRun(void *td)
{Thread *Td = static_cast<Thread *>(td);int cnt = 5;while (cnt){cout << Td->name << " Running..." << "num:" << Td->num << " cnt:" << cnt-- << endl;sleep(1);}delete Td;return (void *)111;
}int main()
{pthread_t pid;Thread *td = new Thread;td->name = "NewThread";td->num = 1;int n = pthread_create(&pid, nullptr, ThreadRun, td);if (n != 0){cerr << "Create New Thread fail..." << endl;}void *retval=nullptr;int m = pthread_join(pid, &retval);if (m == 0){cout << "Main Thread Wait Sucess...New Thread ReturnVal:" << (uint64_t)retval << endl;}
}

 但返回内置类型的值,意义不大:既然传参能用对象传:是不是返回值也能返回对象的地址;这样,不就能给新线程进行派发任务并且将结果进行返回吗!

class ThreadTask
{
public:void print(){cout<<"x + y = "<<result<<endl;};int Task(){return x+y;};
public:string name;int x;int y;int result;
};
void *ThreadRun(void *td)
{ThreadTask *Td = static_cast<ThreadTask *>(td);Td->result=Td->Task();cout << Td->name << " Running..." << endl;return (void *)Td;
}int main()
{pthread_t pid;ThreadTask *td = new ThreadTask;td->name = "NewThread";td->x=10;td->y=20;int n = pthread_create(&pid, nullptr, ThreadRun, td);if (n != 0){cerr << "Create New Thread fail..." << endl;}ThreadTask *returnval=nullptr;int m = pthread_join(pid, (void**)&returnval);if (m == 0){cout << "Main Thread Wait Sucess...;NewThread:";returnval->print();}
}

总结:

a. 只考虑正确的返回不考虑异常:因为异常了,整个进程就崩溃了,包括主线程(线程缺点)

b. 我们可以传递任意类型,但你一定要能想得起来,也可以传递类对象的地址!!!

问题6:如何创建多线程? 

 与多进程类似:主线程for依次创建与等待!

void* ThreadRun(void* name)
{cout << (char*)name << " Running..." << endl;return (void*)name;
}const int gnum=10;
int main()
{pthread_t pid;vector<pthread_t> pids;for(int i=0 ; i < gnum ; i++){char*name =new char[128];//要进行new不然会被覆盖!!snprintf(name,128,"Thread-%d",i+1);pthread_create(&pid,nullptr,ThreadRun,(void*)name);pids.emplace_back(pid);sleep(1);}cout<<"---------------------------"<<endl;//joinfor(auto& pid:pids){void* name=nullptr;pthread_join(pid,(void**)&name);cout<<(char*)name<<"quit..."<<endl;}return 0;
}

现象: 

 

 问题7:新线程如何终止?

a.主线程(main)函数return,进程退出(新线程直接退出)

b.新线程执行的函数return

那如果用exit进行退出呢?是否也是与return 同样的效果?

整个进程直接退出了:所以线程终止不用exit来终止!!

c.使用pthread_exit函数:与return终止是类似的

d. 使用pthread_cancel函数(让主线程去终止新线程)

我们让新线程不退,让主线程进行终止+等待,看看新线程的退出结果是什么? 

void *ThreadRun(void *name)
{while (true){cout << (char *)name << " Running..." << endl;sleep(1);}pthread_exit((void *)name);
}const int gnum = 3;
int main()
{pthread_t pid;vector<pthread_t> pids;for (int i = 0; i < gnum; i++){char *name = new char[128]; // 要进行new不然会被覆盖!!snprintf(name, 128, "Thread-%d", i + 1);pthread_create(&pid, nullptr, ThreadRun, (void *)name);pids.emplace_back(pid);}sleep(2);cout << "---------------------------" << endl;// joinfor (auto &pid : pids){pthread_cancel(pid); // 进行终止void *RetrunVal = nullptr;pthread_join(pid, (void **)&RetrunVal); // 查看返回值cout << (long long int)RetrunVal <<" quit..."<< endl;}return 0;
}

 现象:新线程返回值为-1 ->  #define PTHREAD_CANCELED ((void *) -1)

问题8:可不可以主线程不等待新线程?(像父进程不等待子进程一样)

可以:使用pthread_detach函数

可以自己进行分离

也可以主线程指定新线程进行分离

 

分离后,现象会是什么呢?

void *ThreadRun(void *name)
{// pthread_detach(pthread_self());自己分离while (true){cout << (char *)name << " Running..." << endl;sleep(1);//int* p=nullptr;// *p=10;}pthread_exit((void *)name);
}const int gnum = 3;
int main()
{pthread_t pid;vector<pthread_t> pids;for (int i = 0; i < gnum; i++){char *name = new char[128]; // 要进行new不然会被覆盖!!snprintf(name, 128, "Thread-%d", i + 1);pthread_create(&pid, nullptr, ThreadRun, (void *)name);pids.emplace_back(pid);}// 主线程要进行分离for (auto &pid : pids){pthread_detach(pid);}// 分离后还要join//for (auto &pid : pids)//{//void *RetrunVal = nullptr;//int m = pthread_join(pid, (void **)&RetrunVal); // 查看返回值//cout << (long long int)RetrunVal << " quit... m:" << m << endl;//}sleep(10);return 0;
}

分离了还要join,现象: 

如果分离后不join:主线程往下进行,进程结束:不管线程有没有执行完

如果新线程出问题(野指针),会影响整个进程吗? -> 会的!!

 

3tid

关于tid是什么?先用代码把它打印出来看看:

std::string ToHex(pthread_t tid)
{char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *threadrun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << " is running, tid: " << ToHex(pthread_self()) << std::endl;sleep(1);}
}int main()
{pthread_t tid; // ?pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while (true){std::cout << "main thread running,tid:" << ToHex(tid) << std::endl;sleep(1);}pthread_join(tid, nullptr);return 0;
}

既然tid不等于LWP,那就将它转化为16进制: 

可以看到:tid是一个地址:虚拟地址;那至于是什么,就要到pthread库内部来探索一番:

pthread在还没被加载时,存在磁盘,也是一个文件;

我们用库中的函数:pthread_create创建线程之前,先要把库加载到进程地址空间中!!(与之前讲的动态库加载一毛一样)

既然创建线程要找到库才能实现:那么pthread库内部就要对线程进行管理!

a.如何管理?

先描述:库中要先创建描述线程相关结构体的属性级,线程栈大小...

在组织:用类似数组的方式进行统一管理

未来我们只需要找到对应线程块的地址即可对线程进行控制:

而tid本质上就是对应线程的起始地址!! 

b.如何理解?

类比之前学习文件操作时C语言的fopen函数:

FILE也是结构体:struct FILE;{ int fd ; char buffer[]  ...} :但FILE对象实际在哪呢?

在 cstdio 库中;使用fopen函数之前,库中就要为我们malloc出FILE结构体;

创建线程也是一样:Pthread库内部也要malloc出pthread给用户使用

所以在pthread_join时,我们根据tid(地址)找到线程,将返回值void* retunval拷贝到void** returnval中,让用户知道线程完成任务了没有!

如果不等待;进程结束时,所有的执行流关闭,但pthread库中还维护着线程的相关数据及栈结构大小,这会造成内存泄漏;

而进行join时,我们自然也能想到pthread库在进行回收线程的数据以及申请的栈空间

但你不要以为独立的栈结构空间只有对应的线程自己能访问:实际上,这些操作都是在用户空间(栈区,堆区,共享区...)上进行,任何一个线程(执行流)都能看到,都能访问

LInux没有线程的概念,但通过Pthread库提供函数调用,用户层就能使用到线程了,我们把这种现象称为:用户级线程

Linxu线程 = pthread库中线程的属性集 + LWP

 c.pthread库这么保证LWP与内部的pthread结构体对应上?

从OS层面来说:既然Linux只有LWP(轻量级线程),那就注定了OS要提供LWP的系统调用:

在系统调用里有创建LWP的系统调用:

而PThread库里就是对类似的系统调用进行封装!!

4__thread 

Linux中还有一种对变量进行修饰,让各线程都私有化一份变量的关键字:__thread

__thread int g_val = 100;//让g_val在每个线程中都局部储存一份,只在Linux有效;
//int g_val = 100; 不加修饰时g_val共享
std::string ToHex(pthread_t tid)
{char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *threadrun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << " is running, tid: " << ToHex(pthread_self()) << " g_val:" << g_val << " &g_val:" << &g_val << std::endl;sleep(1);}
}int main()
{pthread_t tid; // ?pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while (true){std::cout << "main thread running,tid:" << ToHex(tid) << " g_val:" << g_val << " &g_val:" << &g_val << std::endl;sleep(1);g_val++;}pthread_join(tid, nullptr);return 0;
}

 现象:

四线程封装

对线程进行封装,先描述(对象的属性,即成员变量):

    typedef void (*fun_t)(const std::string &name); // 用户线程执行的回调方法    pthread_t _ptd;//ptdfun_t _func;//回调函数,执行用户的方法std::string _result;//执行方法后,返回结果std::string _name;//线程名bool _IsRun;//当前线程是否处于‘跑’起来的状态

 对象的执行方法就只是对线程的整个生命周期作封装(只是封装线程库函数而已~)

在设计创建线程方法时,可能会有以下问题:

原因:ThreadRun函数在类内部,会存在参数隐含this指针的问题,但我们在传参时又不希望this指针来干扰我们传参:所以我们可以用static修饰就没有了this指针了!

但是如果这样的话我们就不能访问私有成员变量了;这时我们可以将该对象作为参数进行传递:

实现mythread:(描述) 

                         //mythead.hpp#include <string>
#include <pthread.h>namespace mythread
{typedef void (*fun_t)(const std::string &name); // 用户线程执行的回调方法class Thread{public:void Excute(){_IsRun = true; // 线程开始跑起来了//_result = _func(_name); // 线程结束时返回结果_func(_name);}public:Thread(const std::string &name, const fun_t func): _name(name), _func(func){std::cout << _name << " Created..." << std::endl;}static void *ThreadRun(void *args) // 1.设置成static->属于类不属于对象->没有this干扰{//_func(_name);2.写在这里不能访问私有化成员Thread *Self = (Thread *)args; // 获得当前对象Self->Excute(); // 执行用户定义的方法(回调函数)}bool Start(){int n = ::pthread_create(&_ptd, nullptr, ThreadRun, this); // 参数传对象if (n == 0)return true;elsereturn false;}void Stop(){if (_IsRun){::pthread_cancel(_ptd);_IsRun = false;std::cout << _name << " Stoped..." << std::endl;}}void Join(){::pthread_join(_ptd, nullptr);std::cout << _name << " Joined..." << std::endl;}private:pthread_t _ptd;fun_t _func;std::string _result;std::string _name;bool _IsRun;};
}

 创建多线程(用vector进行组织起来)

                           //main.cc
#include <iostream>
#include <vector>
#include <unistd.h>
#include "mythread.hpp"
using namespace mythread;int gnum = 3;void Print(const std::string &name)
{int cnt = 1;while (true){std::cout << name << "is running, cnt: " << cnt++ << std::endl;sleep(1);}
}int main()
{// 先描述:构建线程对象// 在组织:vector!!std::vector<Thread> threads;for (int i = 0; i < gnum; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(name, Print);sleep(1);}// 统一启动for (auto &thread : threads){thread.Start();}sleep(3);std::cout << "-----------------------" << std::endl;// 统一结束for (auto &thread : threads){thread.Stop();}std::cout << "-----------------------" << std::endl;// 等待线程等待for (auto &thread : threads){thread.Join();}
}

测试:一次创建3个线程

五线程互斥 

1多线程访问

见一见多线程访问存在的问题

int tickets = 10000;void* route(void* name)
{while(true){if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", (char*)name, tickets);tickets--;}else{break;}}
}int main()
{pthread_t pid;for(int i=0;i<5;i++){char *name = new char[128]; snprintf(name, 128, "Thread %d", i+1);pthread_create(&pid, nullptr, route, (void *)name);}for(int i=0;i<5;i++){pthread_join(pid,nullptr);}return 0;
}

五个线程同时进行抢10000张票时,现象: 

 明明只有10000张票,怎么会抢到-1张呢?

这要深入CPU层面上来解释现象:

CPU在执行线程a的代码,进行循环进行判断时;

1.CPU要先把数据加载到寄存器eax中;2.进行逻辑运算;3将结果保存在寄存器中进行返回

因为线程时并发执行的;这几步中都可能有线程来进行切换;被切换时,它要带走自己的寄存器数据;等到再次被调度时,才进行恢复(上下文)

 (tickets=1时)线程a进行到第二步时,把tickets的变量进行--后,就要将数据进行返回时:CPU被切换了,去执行线程b的代码了,由于线程a没有把tickets--后的值进行返回,此时线程b看到的tickets还是1:判断为真,可以抢到票啦!tickets--;(如果有很多线程,结果是不是更恐怖-=-)

当重新调度线程a时,将结果返回,此时的tickets不是=0,而是-1:

也就是说:一张票被两个线程同时抢到了!!

这也就是多线程访问时存在的问题:数据不一致

2认识锁与对应接口

所谓的对资源进行保护,本质上是对临界区资源作保护

而我们在对资源进行访问时,本质上是通过代码进行访问

总结:对临界区资源进行保护,就是对访问资源代码进行保护起来,即加锁!!

3锁的种类

1全局锁

用全局锁的方式来解决

using namespace std;int tickets = 10000;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;//全局锁
void* route(void* name)
{//pthread_mutex_lock(&gmutex);保证粒子性while(true){pthread_mutex_lock(&gmutex);if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", (char*)name, tickets);tickets--;pthread_mutex_unlock(&gmutex);//解锁}else{pthread_mutex_unlock(&gmutex);//跳出判断也要解锁break;}}
}int main()
{pthread_t pid;for(int i=0;i<5;i++){char *name = new char[128]; snprintf(name, 128, "Thread %d", i+1);pthread_create(&pid, nullptr, route, (void *)name);}for(int i=0;i<5;i++){pthread_join(pid,nullptr);}return 0;
}

现象是:抢票的过程变慢了;刚好抢完10000张票!! 

 2局部锁

用局部锁的方式来解决:要让所有的线程都能看到这个锁(封装成类对象ThreadLock

(这里用之前封装线程的类对象方式创建线程)

// mythread.hpp
#pragma once
namespace mythread
{class ThreadLock{public:ThreadLock(const std::string& name,pthread_mutex_t *lock)//传局部锁的地址:_name(name),_lock(lock){}std::string&GetName(){return _name;}pthread_mutex_t *GetLock(){return _lock;}private:std::string _name; pthread_mutex_t *_lock;};typedef void (*fun_t)(ThreadLock* tk); // 用户线程执行的回调方法class Thread{public:void Excute(){_IsRun = true; // 线程开始跑起来了//_result = _func(_name); // 线程结束时返回结果_func(_tk);}public:Thread(const std::string &name, const fun_t func,ThreadLock* tk): _name(name), _func(func),_tk(tk){std::cout << _name << " Created..." << std::endl;}static void *ThreadRun(void *args) // 1.设置成static->属于类不属于对象->没有this干扰{//_func(_name);2.写在这里不能访问私有化成员Thread *Self = (Thread *)args; // 获得当前对象Self->Excute(); // 执行用户定义的方法(回调函数)}bool Start(){int n = ::pthread_create(&_ptd, nullptr, ThreadRun, this); // 参数传对象if (n == 0)return true;elsereturn false;}void Stop(){if (_IsRun){pthread_cancel(_ptd);_IsRun = false;std::cout << _name << " Stoped..." << std::endl;}}void Join(){pthread_join(_ptd, nullptr);std::cout << _name << " Joined..." << std::endl;delete _tk;}private:pthread_t _ptd;fun_t _func;std::string _result;std::string _name;bool _IsRun;ThreadLock* _tk;};
}//LockGuard.hpp(实现自动化加锁与解锁)
#pragma onceclass LockGuard
{
public:LockGuard(pthread_mutex_t *tk):_tk(tk){//创建加锁pthread_mutex_lock(_tk);}~LockGuard(){//析构解锁pthread_mutex_unlock(_tk);}private:pthread_mutex_t *_tk;
};//test.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <vector>#include"mythread.hpp"
using namespace mythread;
#include"LockGuard.hpp"int tickets = 1000;void route(ThreadLock* tk)
{while(true){//创建匿名对象,出循环自动销毁LockGuard lockguard(tk->GetLock());//pthread_mutex_lock(tk->GetLock());if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", tk->GetName().c_str(), tickets);tickets--;//pthread_mutex_unlock(tk->GetLock());//解锁}else{//pthread_mutex_unlock(tk->GetLock());//跳出判断也要解锁break;}}
}const int gThreadNum=6;
int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);std::vector<Thread> tids; for(int i=0;i<gThreadNum;i++){std::string name="thread-"+std::to_string(i+1);ThreadLock* tk=new ThreadLock(name,&mutex);tids.emplace_back(name,route,tk);}for(auto& tid:tids){tid.Start();}for(auto& tid:tids){tid.Join();}pthread_mutex_destroy(&mutex);return 0;
}

现象:也是能完成抢票逻辑的!

 4理解锁

加锁与解锁不能是在while循环的前面与循环结束(会导致只有一个线程抢到全部票)

1.加锁的范围,粒度一定要尽量适当

问题:能不能让其它线程去竞争锁,单独的一个线程不去与它们竞争,直接进行抢票?

原理上是行得通的(毕竟代码是你写的),当这不就是程序员自己写出的bug吗!

2.但如何线程去进行抢票,都要申请锁:原则上不允许有例外!

3.所有线程都要申请锁,前提时每个线程都要看到锁;

(在这里)锁定义在全局,本身是共享资源——加锁的过程:必须是原子性

(原子性:要么申请成功,要么申请失败,没有正在申请的动作,具有两态性)

4.如果线程申请锁失败了,那么线程就阻塞在那;如果申请成功了,就继续往下执行

5.那如果线程申请成功了,执行临界区的代码,在访问资源的过程中能被切换吗?

可以切换(如时间片到了会被切换);

但是其它线程照样无法访问临界区资源(代码):我虽然被切换了,但没有释放锁啊!

在这个过程中我可以放心的执行临界区代码到结束,没人可以干扰我!

所以:对于其它线程:要么我没申请锁,要么我释放了锁:这两种结果才对其它线程有意义

也就是说:我访问临界区,对其它线程是具有原子性的

理解:pthread_mutex_lock(&mutex)函数申请锁成功与失败

线程进行申请锁的过程只会有两种结果:

a.申请成功,pthread_mutex_lock()函数返回,往下执行临界区的代码

b.申请失败时,pthread_mutex_lock()函数不返回,线程就阻塞了;要记住:对线程的操作都是由pthread库提供的;线程阻塞了,pthread库就会去进行(调用相关函数)将该线程由R状态设置成S状态,放到等待队列中:知道申请成功的线程进行pthread_mutex_unlock()函数解锁;pthread库就将阻塞的线程全部唤醒,进行去申请锁...

5实现角度理解锁

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令

作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

在接下去看之前,我们先来认识三个点:(与其说是认识,不如说时总结)

1.CPU的寄存器只有一套,但寄存器里面的数据可以有多套:这些数据属于执行流说私有的!
2.CPU在执行代码时,一定是要有对应的载体的!

3.数据在内容中是被所有的线程所共享的!

现在我们把lock和unlock的指令改成伪指令,与CPU执行的逻辑简单描述下:

 注意:这里的第二行的xchgb指令是直接将%al与mutex的数据进行交换,不存在中间变量的转换

6可重入VS线程安全

*线程安全:多个线程并发同一段代码时,不会出现不同的结果

常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

*重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

一句话:可重入一定是线程安全,而线程安全不一定可重入(当临界资源解锁就一定可重入)

7死锁  

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。 (各自持有锁)

7.1必要条件

互斥条件:一个资源每次只能被一个执行流使用(各自占有锁不释放)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(协商失败)
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 (赖着不走)
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 (等你释放锁)

7.2避免死锁

~破坏死锁的必要条件
~加锁顺序一致
~避免锁未释放的场景
~资源一次性分配

 六线程同步

以一段故事来引入:

今天在你们学校的图书馆,有一个VIP自习室:进去里面的钥匙就挂在门口,而且规定每次只能进去一个人;你是一个热爱学习的少年,早早地就来到自习室门口,拿着钥匙就进去里面学习了;过了一段时间,三五成群的人也走了过来,也想来里面学习,但发现门口没钥匙,就知道里面已经有人了,没办法就只能在门外等;你在里面学了一段时间突然想上厕所,就拿着钥匙开了门走了出去;在你上厕所的这段时间,别人能进行里面学习吗??

答案是不行的:钥匙在你手上,别人根本进不去!(门锁住了)

从早上八点学到下午1点,你发现你有点饿了,想着是时候去食堂吃饭了:但当你走到门口时,发现有很多人都在等待你;你心想:要是去吃饭等下在回来的时候都不知道要等到什么时候才能再次进去,你就干脆就不吃了,继续锁门学习;学了一会发现很饿,有走到门口又重复刚才的动作...

其他人长时间无法进到自习室中学习 = 线程无法获取临界资源  - >造成饥饿问题

但你重复着同样的动作,有错吗??违法了规则了吗??

好像没有吧(不怎么道德就是了~)!!但是不合理,也就是不高效(你学不到,别人也是)

这时就有其中一个同学打电话给图书馆管理员来解决问题,管理员修改了规则:

每一个自习完成的同学,归还钥匙后:

1.不能里面申请;

2.第二次申请,必须排队(换句话说:其他人也必须排队)

规则发生了变化:这样就能保证所有人在访问自习室的过程,是有顺序性的!!

(而顺序性不一定是严格的顺序性,也可以是相对的顺序性(队伍中随机抽人进行访问)

也就是同步!!

1条件变量 

a.快速地认识接口

b.认识条件变量

也是以一段故事来形象地认识条件变量:

今天你(线程A)和你的朋友(线程B)在玩一个游戏:玩期间不许说话,你要把眼睛用眼罩蒙上,你的朋友的任务是将苹果刚好放到盘子(共享资源)里,你的任务是把盘子拿起来并用另一只手将苹果拿到,这个苹果才算是你的;在玩的过程中,由于你并不知道他什么时候回将苹果放到盘子上,你就快速的摸着桌子把盘子给拿到(lock),但发现里面没有苹果,又将盘子给放下(unlock);但你发现不对,又把盘子给拿到,发现没有又放回去...由于你的不确定+多次拿起盘子,你的朋友在旁边很根本没有合适的时间将苹果放到盘子上,干着急但又不能说话~

玩了十几分钟,你颗粒无收;而你的朋友也无能为力,就同你商量着制定出一个规则:用一个铃铛(条件变量):你把盘子放下时就按下铃铛;朋友把苹果放到盘子了就按下铃铛;

这样你就不用担心苹果是什么时候放到盘子里的了;朋友也能顺利的把苹果放到盘子里,放完后就按下铃铛来告诉你(唤醒线程A

通过这个游戏你得到了很多苹果,拿到宿舍与舍友分享,你的舍友问你在哪得到的,你就把此时告诉他们;这一次来玩的就不止是你一人了,而是由几个人来进行拿苹果(线程C,线程D...),你拿到苹果后站在铃铛旁就要进行排队了(等待过程不一定是严格的顺序性!);你的苹果把铃铛按下时,既可以是在通知你(唤醒线程),也可以是通知全部人(唤醒所有线程

c.测试代码

实现:创建5个线程(+主线程),让主线程控制线程的执行顺序:

#include <iostream>
#include <pthread.h>
#include <unistd.h>const static int nums = 5;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;void *wait(void *args)
{char *name = static_cast<char *>(args);while (true){pthread_mutex_lock(&gmutex);pthread_cond_wait(&gcond, &gmutex);printf("I am %s\n", name);pthread_mutex_unlock(&gmutex);usleep(10000);}
}int main()
{pthread_t threads[nums];for (int i = 0; i < nums; i++){char *name = new char[1024];snprintf(name, 1024, "thread-%d", i + 1);pthread_create(&threads[i], nullptr, wait, name);usleep(10000);}// 主线程进行控制while (true){pthread_cond_signal(&gcond);std::cout << "Wake A Thread..." << std::endl;//pthread_cond_broadcast(&gcond);//std::cout << "Wake Threads..." << std::endl;sleep(1);}for (int i = 0; i < nums; i++){pthread_join(threads[i], nullptr);}return 0;
}

 

七生产消费模型

1认识生产消费模型

拿生活例子来说:晚上肚子饿了,突然想吃方便面,你是不是就得起身去超市买啊:而超市上摆放的各种方便面种类从哪来呢?是不是从各地供货商进货的呢!

在上面的这个例子中,你的角色是消费者,而供货商是生产者

有没有可能你不在超市买,转而到生产方便面的工厂去买?为了生产出来这包方便面,就要启动这一条超长的流水线来生产,卖的钱还不够交电费呢~

而有了超市这个临时储存(缓存)点就恰好解决这个问题:在你来买方便面时我有产品来供应你,而产品不够时就打电话叫供货商来补充货物,做到忙闲不均

而这也方便我们日常消费(走几步就到超市门口了),而工厂通常在偏僻的地方;在你买到方便面回到宿舍开始边吃边感叹命运不公时,也可能这时超市在补充产品:做到并发,也就是效率高~

而当一个供货方经营不善倒闭时丝毫不影响超市的进货(有其他供应商可选择),做到解耦

总结:321原则

1.一个交易场所(内存空间)

2.两种角色(生产者,消费者)

3.三种关系(生产者-生产者,消费者-消费者,生产者-消费者)

解释:生产者与生产者是互斥关系:很好理解,供应商恨不得所有超市都是我一个人来供应

而消费者和消费者呢?也是互斥关系~ 这时就有人说了:不对啊,超市里有很多消费者不是都是在有序地进行购物吗?这么是互斥关系;这时因为超市里的商品多种类多:如果这家超市就只有一盒方便面了,两者都同时想要买方便面的消费者不就是互斥关系吗!

生产者与消费者的关系是:互斥&&同步:互斥体现在产品还没入库呢你就来进行消费,这不就是零元购吗?同步体现在我要去买方便面时刚好超市卖完了,过了几个天来买发现还是没有,这会造成饥饿问题(概念);超市里产品没人消费也不行:要求两者之间存在同步关系~

 2实现生产消费模型

实现本质是遵守321原则,维护三种关系,用锁与条件变量来进行实现~ 

思路:一个线程往队列生产任务,一个线程往队列消费任务(任务进行处理)

//Main.cc
#include <iostream>
#include <pthread.h>
#include <queue>
#include <ctime>
#include <string>
#include <unistd.h>
using namespace std;#include "queue.hpp"
#include "task.hpp"void *Productor(void *arg)
{srand(time(nullptr));Market<Task> *mt = static_cast<Market<Task> *>(arg);while (true){int x = rand() % 100;int y = rand() % 100;Task tk(x,y);mt->push(tk);//cout << "Product: " << x << endl;tk.ProdectorPrint();sleep(1);}
}void *Consumer(void *arg)
{Market<Task> *mt = static_cast<Market<Task> *>(arg);while (true){Task result;mt->pop(&result);//result.Excute();result();result.ConsumerPrint();sleep(1);}
}int main()
{pthread_t c, p;Market<Task> *mt = new Market<Task>();pthread_create(&p, nullptr, Productor, (void *)mt);pthread_create(&c, nullptr, Consumer, (void *)mt);pthread_join(p, nullptr);pthread_join(c, nullptr);return 0;
}//queue.hpp
#pragma onceconst int maxcap=5;template<class T>
class Market
{
public:Market(){pthread_mutex_init(&mutex,nullptr);pthread_cond_init(&c_cond,nullptr);pthread_cond_init(&p_cond,nullptr);}~Market(){pthread_mutex_destroy(&mutex);pthread_cond_destroy(&c_cond);pthread_cond_destroy(&p_cond);}//生产者void push(T& in){pthread_mutex_lock(&mutex);while(qe.size()==maxcap)//if不具有健壮性(如果是if唤醒后会直接往下执行代码){pthread_cond_wait(&p_cond,&mutex);//等待会释放锁;唤醒会参与锁的竞争}qe.push(in);pthread_mutex_unlock(&mutex);//让消费者消费pthread_cond_signal(&c_cond);}//消费者void pop(T* out){pthread_mutex_lock(&mutex);while(qe.size()==0){pthread_cond_wait(&c_cond,&mutex);}*out=qe.front();qe.pop();pthread_mutex_unlock(&mutex);//让生产者生产pthread_cond_signal(&p_cond);}private:queue<T> qe;pthread_mutex_t mutex;pthread_cond_t c_cond;pthread_cond_t p_cond;
};//task.hpp
#pragma onceclass Task
{
public:Task(){}Task(const int &x, const int &y): _x(x), _y(y){}void Excute(){_result = _x + _y;}void operator()(){Excute();}void ProdectorPrint(){cout<<to_string(_x) + " +" + to_string(_y) + " =" + " ?"<<endl;}void ConsumerPrint(){cout<<to_string(_x) + " +" + to_string(_y) + " =" + to_string(_result)<<endl;}private:int _x;int _y;int _result;
};

现象: 

我们在前面说的效率高,在这里怎么体现出来呢?

在生产任务之前是不是要花时间来产生任务,布置任务?

在消费任务之前是不是要花时间来获取任务,挑选任务?

在我们的代码中,有没有可能其它生产者在产生任务时,我(生产者)在生产任务?

在其它消费者在获取任务时,我(消费者)在消费任务?

如果这样想的话,是不是这就并发起来了呢?这不就是得效率高起来了吗!

所以:如果生产花费时间多,消费花费时间少,我们可以选择多生产单消费

如果生产花费时间少,消费花费时间多,我们可以选择单生产多消费...

3读写锁

我们先来认识到什么是读写者问题:

与生产消费模型类似:我们可以用321原则来描述它:

1:一种交易场所-->黑板

2:两种角色-->读者与写者

3:三种关系-->读者与读者(互斥),写者与写者(互斥),读者与写者(互斥&&并发)

与生产消费模型不同的是:在不同角色的关系上它不是同步,而是并发:怎么理解?

在里面学校:你是负责画黑板报的人(写者),而你的同学(读者)可以继续黑板报的阅读,可以是一个人,也可以是一群人一起阅读(并发);

你在绘画时是不允许有人在进行阅读的;同样在同学进行阅读时是不允许你进行绘画(互斥)

 3.1理解读写者问题

公共部分

C++
uint32_t reader_count = 0;
lock_t count_lock;
lock_t writer_lock;

Reader

//加锁逻辑
lock(count_lock);//加锁,让写者进行等待
if(reader_count == 0)lock(writer_lock);//writer加锁,让读者进行等待
++reader_count;
unlock(count_lock);//写者访问临界资源//解锁逻辑
lock(count_lock);
--reader_count;
if(reader_count == 0)unlock(writer_lock);
unlock(count_lock);

Writer

lock(writer_lock);
// 临界资源
unlock(writer_lock);

 3.2测试代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstdlib>
#include <ctime>// 共享资源
int shared_data = 0;// 读写锁
pthread_rwlock_t rwlock;// 读者线程函数
void *Reader(void *arg)
{//sleep(1); //读者优先,一旦读者进入&&读者很多,写者基本就很难进入了int number = *(int *)arg;while (true){pthread_rwlock_rdlock(&rwlock); // 读者加锁std::cout << "读者-" << number << " 正在读取数据, 数据是: " << shared_data << std::endl;sleep(1);                       // 模拟读取操作pthread_rwlock_unlock(&rwlock); // 解锁}delete (int*)arg;return NULL;
}// 写者线程函数
void *Writer(void *arg)
{int number = *(int *)arg;while (true){pthread_rwlock_wrlock(&rwlock); // 写者加锁shared_data = rand() % 100;     // 修改共享数据std::cout << "写者- " << number << " 正在写入. 新的数据是: " << shared_data << std::endl;sleep(2);                       // 模拟写入操作pthread_rwlock_unlock(&rwlock); // 解锁}delete (int*)arg;return NULL;
}int main()
{srand(time(nullptr)^getpid());pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁// 可以更高读写数量配比,观察现象const int reader_num = 2;const int writer_num = 2;const int total = reader_num + writer_num;pthread_t threads[total]; // 假设读者和写者数量相等// 创建读者线程for (int i = 0; i < reader_num; ++i){int *id = new int(i);pthread_create(&threads[i], NULL, Reader, id);}// 创建写者线程for (int i = reader_num; i < total; ++i){int *id = new int(i - reader_num);pthread_create(&threads[i], NULL, Writer, id);}// 等待所有线程完成for (int i = 0; i < total; ++i){pthread_join(threads[i], NULL);}pthread_rwlock_destroy(&rwlock); // 销毁读写锁return 0;
}

现象

造成写者饥饿问题:也只是局限于以上代码会出现:通常读者读取数据把锁解开是会有一段时间来处理数据,此时写者是有机会进行写的,不用担心~

如果将将上面代码中的read_num=1,write_num=5,这时:

4自旋锁 

自旋锁是一种多线程同步机制, 用于保护共享资源免受并发访问的影响。 在多个线程尝试获取锁时, 它们会持续自旋(即在一个循环中不断检查锁是否可用) 而不是立即进入休眠状态等待锁的释放。(线程等待的时间决定自旋的方式)

4.1原理

自旋锁通常使用一个共享的标志位(如一个布尔值) 来表示锁的状态。

当标志位为true 时, 表示锁已被某个线程占用(继续循环); 当标志位为 false 时, 表示锁可用。(退出循环)

C++
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <unistd.h>
// 使用原子标志来模拟自旋锁
atomic_flag spinlock = ATOMIC_FLAG_INIT; // ATOMIC_FLAG_INIT 是 0// 尝试获取锁
void spinlock_lock() 
{
while (atomic_flag_test_and_set(&spinlock)) {
// 如果锁被占用, 则忙等待} 
} // 释放锁
void spinlock_unlock() 
{
atomic_flag_clear(&spinlock);
} typedef _Atomic struct
{#ifif __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1_Bool __val;
#elseunsigned char __val;
#endif
}atomic_flag;

4.2测试代码 

拿前面的抢票代码来测试,你会发现与上面互斥锁实现是一模一样的~

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
pthread_spinlock_t lock;void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_spin_lock(&lock);if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_spin_unlock(&lock);}else{pthread_spin_unlock(&lock);break;}}return nullptr;
}int main(void)
{pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_spin_destroy(&lock);
}

 4.3使用场景

1. 短暂等待的情况: 适用于锁被占用时间很短的场景, 如多线程对共享数据进行简
单的读写操作。(相反:长时间等待会出现活锁问题,CPU资源浪费)
2. 多线程锁使用: 通常用于系统底层, 同时 CPU 对共享资源的访问。(减少系统开销)

八信号量 

信号量在前面的文章中有介绍到:进程间通信:这里就直接对信号量进行使用

1相关接口 

2实现

问题:在前面的实现生产消费模型中,我们为什么要把条件变量放在锁(临界区)中进行等待呢?

因为无论是消费者还是生产者,我要到里面在知道是否有空间(产品)来让我生产(消费):也就是判断这个状态得是进到临界区中才知道的,这就注定了等待是在临界区里等待!

实现思路:构造一个环形队列,定义两个指针(c_step p_step)来进行生产消费

解决问题:

指针指向同一位置时,此时队列为空还是队列满了?

不指向同一位置时,怎么保证生产消费同时进行?

队列为空让生产者先生产,队列满了让消费者先消费,这也要有先后顺序?

让信号量来保证!!

//Main.cc
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
#include <unistd.h>#include "task.hpp"#include "RingQueue.hpp"void *product(void *args)
{RingQueue<Task> *re = static_cast<RingQueue<Task> *>(args);while (true){// 添加数据int x = rand() % 10 + 1;int y=rand()%100+1;Task t(x,y);// 生产re->Push(t);t.ProdectorPrint();}
}void *consume(void *args)
{RingQueue<Task> *re = static_cast<RingQueue<Task> *>(args);while (true){// 拿数据Task result;re->Pop(&result);// 进行消费result.ConsumerPrint();sleep(1);}
}int main()
{srand(time(NULL));pthread_t producer, consumer;RingQueue<int> *re = new RingQueue<int>();pthread_create(&producer, nullptr, product, re);pthread_create(&consumer, nullptr, consume, re);pthread_join(producer, nullptr);pthread_join(consumer, nullptr);return 0;
}//RingQueue.hpptemplate<typename T>
class RingQueue
{
public:RingQueue():_max_cap(5),_c_step(0),_p_step(0){_ringqueue.resize(_max_cap);sem_init(&_date,0,0);sem_init(&_space,0,_max_cap);pthread_mutex_init(&_product,nullptr);pthread_mutex_init(&_consume,nullptr);}void Push(const T&in){sem_wait(&_space);//P操作 先并发预定资源pthread_mutex_lock(&_product);_ringqueue[_p_step]=in;_p_step++;_p_step%=_max_cap;sem_post(&_date);//V操作pthread_mutex_unlock(&_product);}void Pop(T* out){sem_wait(&_date);pthread_mutex_lock(&_consume);*out=_ringqueue[_c_step];out->Excute();_c_step++;_c_step%=_max_cap;sem_post(&_space);pthread_mutex_unlock(&_consume);}~RingQueue(){sem_destroy(&_date);sem_destroy(&_space);pthread_mutex_destroy(&_product);pthread_mutex_destroy(&_consume);}
private:std::vector<T> _ringqueue;int _max_cap;sem_t _date;sem_t _space;int _p_step;int _c_step;pthread_mutex_t _product;pthread_mutex_t _consume;
};//task.hpp
#pragma once
using namespace std;class Task
{
public:Task(){}Task(const int &x, const int &y): _x(x), _y(y){}void operator()(){Excute();}int Excute(){_result = _x + _y;}void ProdectorPrint(){cout<<"Prodecter->"<<to_string(_x) + " +" + to_string(_y) + " =" + " ?"<<endl;}void ConsumerPrint(){cout<<"Conseumer->"<<to_string(_x) + " +" + to_string(_y) + " =" + to_string(_result)<<endl;}
private:int _x;int _y;int _result;
};

问题

为什么信号申请资源时不用进行判断?  -->  信号量本身是判断条件

信号量:本质是一个计算器,是对资源的预定:在外部可以不判断就知道内部资源情况!

以上资源不整体使用的情况:而资源整体使用时,使用信号量就等同于使用锁+条件变量,但这时的信号量我们通常叫二元信号量~

九线程池

实现思路:用vector容器来储存要创建线程的个数,用queue来push任务(接收发来的任务),随机派发线程进行任务的处理(用上面封装好的task来模拟发来的任务)

//ThreadPool.hpp
#pragma once#include "Mythread.hpp"
using namespace mythread;const static int gnum = 5;template <typename T>
class ThreadPool
{
private:void Lock(){pthread_mutex_lock(&_mutex);}void UnLock(){pthread_mutex_unlock(&_mutex);}void Sleep(){pthread_cond_wait(&_cond, &_mutex);}void Wake(){pthread_cond_signal(&_cond);}void WakeAll(){pthread_cond_broadcast(&_cond);}void Excute(){while (true){Lock();while (_task.empty() && _isrunning){_sleep_num++;Sleep();_sleep_num--;}// 没任务要退出了if (_task.empty() && !_isrunning){UnLock();//先解锁break;}// 有任务继续执行T t = _task.front();_task.pop();UnLock();// 在外面处理任务t();}}public:ThreadPool(int num = gnum): _num(num), _sleep_num(0),_isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}void Init(){for (int i = 0; i < _num; i++){std::string name = "thread-" + std::to_string(i + 1);_threads.emplace_back(name, std::bind(&ThreadPool::Excute, this));}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();}}void Push(const T &in){Lock();if (_isrunning){_task.push(in);if (_sleep_num > 0)Wake();}UnLock();}void Stop(){Lock();_isrunning = false;WakeAll();UnLock();}private:int _num;std::vector<Thread> _threads;std::queue<T> _task;bool _isrunning;int _sleep_num;pthread_mutex_t _mutex;pthread_cond_t _cond;
};//Mythread.hpp
#pragma oncenamespace mythread
{//typedef void (*fun_t)(const std::string &name); // 用户线程执行的回调方法using fun_t=std::function<void()>;class Thread{public:void Excute(){_IsRun = true; // 线程开始跑起来了//_result = _func(_name); // 线程结束时返回结果_func();}public:Thread(const std::string &name, const fun_t func): _name(name), _func(func){std::cout << _name << " Created..." << std::endl;}static void *ThreadRun(void *args) // 1.设置成static->属于类不属于对象->没有this干扰{//_func(_name);2.写在这里不能访问私有化成员Thread *Self = (Thread *)args; // 获得当前对象Self->Excute(); // 执行用户定义的方法(回调函数)return nullptr;}bool Start(){int n = ::pthread_create(&_ptd, nullptr, ThreadRun, this); // 参数传对象if (n == 0)return true;elsereturn false;}void Stop(){if (_IsRun){pthread_cancel(_ptd);_IsRun = false;std::cout << _name << " Stoped..." << std::endl;}}void Join(){pthread_join(_ptd, nullptr);std::cout << _name << " Joined..." << std::endl;}private:pthread_t _ptd;fun_t _func;std::string _result;std::string _name;bool _IsRun;};
}//Task.hpp
#pragma onceclass Task
{
public:Task(){}Task(const int &x, const int &y): _x(x), _y(y){}void operator()(){Excute();}void Excute(){_result = _x + _y;ConsumerPrint();}void ProdectorPrint(){std::cout<<"Prodecter->"<<std::to_string(_x) + " +" + std::to_string(_y) + " =" + " ?"<<std::endl;}void ConsumerPrint(){std::cout<<"Conseumer->"<<std::to_string(_x) + " +" + std::to_string(_y) + " =" + std::to_string(_result)<<std::endl;}int _x;int _y;int _result;
};//Main.cc
#include <iostream>
#include <memory>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <queue>#include "task.hpp"
#include "ThreadPool.hpp"int main()
{std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>();//c++14tp->Init();tp->Start();while (true){Task t(1,1);tp->Push(t);sleep(1);}tp->Stop();return 0;
}

十日志

我们在用线程池测试时要用到很多的cout,endl,非常的不方便,所以我们可以封装一个日志来代替它~

未来日志的格式:

[日志等级][id][文件名][行号][当前时间] 内容

// log.hpp#pragma onceenum
{DEBUG = 1,INFO,WARNING,ERROR,FATAL
};std::string Getlevel(int level)
{switch (level){case DEBUG:return "DEBUG";break;case INFO:return "INFO";break;case WARNING:return "WARNING";break;case ERROR:return "ERROR";break;case FATAL:return "FATAL";break;default:return "";break;}
}std::string Gettime()
{time_t now = time(nullptr);struct tm *time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",time->tm_year + 1900,time->tm_mon + 1,time->tm_mday,time->tm_hour,time->tm_min,time->tm_sec);return buffer;
}struct log_message
{std::string _level;int _id;std::string _filename;int _filenumber;std::string _cur_time;std::string _message;
};#define SCREAM 1
#define FILE 2#define DEVELOP 3
#define OPERATION 4const std::string gpath = "./log.txt";
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;class log
{
public:log(const std::string &path = gpath, const int status = DEVELOP): _mode(SCREAM), _path(path), _status(status){}void SelectMode(int mode){_mode = mode;}void SelectStatus(int status){_status = status;}void PrintScream(const log_message &le){printf("[%s][%d][%s][%d][%s] %s",le._level.c_str(),le._id,le._filename.c_str(),le._filenumber,le._cur_time.c_str(),le._message.c_str());}void PrintFile(const log_message &le){std::fstream in(_path, std::ios::app);if (!in.is_open())return;char buffer[1024];snprintf(buffer, sizeof(buffer), "[%s][%d][%s][%d][%s] %s",le._level.c_str(),le._id,le._filename.c_str(),le._filenumber,le._cur_time.c_str(),le._message.c_str());in.write(buffer, strlen(buffer)); // 不用sizeofin.close();}void PrintLog(const log_message &le){// 过滤if (_status == OPERATION)return;// 线程安全pthread_mutex_lock(&gmutex);switch (_mode){case SCREAM:PrintScream(le);break;case FILE:PrintFile(le);break;default:break;}pthread_mutex_unlock(&gmutex);}void logmessage(int level, const std::string &filename, int filenumber, const char *message, ...){log_message le;le._level = Getlevel(level);le._id = syscall(SYS_gettid);le._filename = filename;le._filenumber = filenumber;le._cur_time = Gettime();va_list vt;va_start(vt, message);char buffer[128];vsnprintf(buffer, sizeof(buffer), message, vt);va_end(vt);le._message = buffer;// 打印日志PrintLog(le);}~log(){}private:int _mode;std::string _path;int _status;
};// 方便上层调用
log lg;// ##不传时可忽略参数
#define LOG(level, message, ...)                                          \do                                                                    \{                                                                     \lg.logmessage(level, __FILE__, __LINE__, message, ##__VA_ARGS__); \} while (0)#define SleftScream()          \do                         \{                          \lg.SelectMode(SCREAM); \} while (0)
#define SleftFile()          \do                       \{                        \lg.SelectMode(FILE); \} while (0)#define SleftDevelop()            \do                            \{                             \lg.SelectStatus(DEVELOP); \} while (0)
#define SleftOperation()            \do                              \{                               \lg.SelectStatus(OPERATION); \} while (0)//Mythread.hpp
#pragma oncenamespace mythread
{//typedef void (*fun_t)(const std::string &name); // 用户线程执行的回调方法using fun_t=std::function<void()>;class Thread{public:void Excute(){_IsRun = true; // 线程开始跑起来了//_result = _func(_name); // 线程结束时返回结果_func();}public:Thread(const std::string &name, const fun_t func): _name(name), _func(func){std::cout << _name << " Created..." << std::endl;}static void *ThreadRun(void *args) // 1.设置成static->属于类不属于对象->没有this干扰{//_func(_name);2.写在这里不能访问私有化成员Thread *Self = (Thread *)args; // 获得当前对象Self->Excute(); // 执行用户定义的方法(回调函数)return nullptr;}bool Start(){int n = ::pthread_create(&_ptd, nullptr, ThreadRun, this); // 参数传对象if (n == 0)return true;elsereturn false;}void Stop(){if (_IsRun){pthread_cancel(_ptd);_IsRun = false;std::cout << _name << " Stoped..." << std::endl;}}void Join(){pthread_join(_ptd, nullptr);std::cout << _name << " Joined..." << std::endl;}private:pthread_t _ptd;fun_t _func;std::string _result;std::string _name;bool _IsRun;};
}//ThreadPool.hpp
#pragma once#include "Mythread.hpp"
using namespace mythread;const static int gnum = 5;
namespace threadpool
{void Lock(pthread_mutex_t &_mx){pthread_mutex_lock(&_mx);}void UnLock(pthread_mutex_t &_mx){pthread_mutex_unlock(&_mx);}template <typename T>class ThreadPool{private:void Sleep(){LOG(DEBUG, "Thread Sleep\n");pthread_cond_wait(&_cond, &_mutex);}void Wake(){LOG(DEBUG, "Thread Wake\n");pthread_cond_signal(&_cond);}void WakeAll(){pthread_cond_broadcast(&_cond);}void Excute(){while (true){Lock(_mutex);while (_task.empty() && _isrunning){_sleep_num++;Sleep();_sleep_num--;}// 没任务要退出了if (_task.empty() && !_isrunning){UnLock(_mutex); // 先解锁LOG(DEBUG, "Thread Quit\n");break;}// 有任务继续执行T t = _task.front();_task.pop();UnLock(_mutex);// 在外面处理任务t();LOG(DEBUG, t.Result().c_str());}}ThreadPool(int num = gnum): _num(num), _sleep_num(0), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;void operator=(const ThreadPool<T> &) = delete;void Init(){for (int i = 0; i < _num; i++){std::string name = "thread-" + std::to_string(i + 1);_threads.emplace_back(name, std::bind(&ThreadPool::Excute, this));}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();}}public:// 单例模式->懒汉模式static ThreadPool<T> *GetInstance(){if (_tp == nullptr){LOG(DEBUG, "Creat Threadpool\n");Lock(_sig_mutex);if (_tp == nullptr){_tp = new ThreadPool(); // new 对象出来_tp->Init();_tp->Start();}UnLock(_sig_mutex);}else{LOG(DEBUG, "Get Threadpool\n");}return _tp;}void Push(const T &in){Lock(_mutex);if (_isrunning){_task.push(in);if (_sleep_num > 0)Wake();}UnLock(_mutex);}void Stop(){Lock(_mutex);_isrunning = false;WakeAll();UnLock(_mutex);}void Join(){for(auto& thread:_threads){thread.Join();}}private:int _num;std::vector<Thread> _threads;std::queue<T> _task;bool _isrunning;int _sleep_num;pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *_tp;static pthread_mutex_t _sig_mutex;};template <typename T>ThreadPool<T> *ThreadPool<T>::_tp = nullptr;template <typename T>pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
}//Task.hpp
#pragma onceclass Task
{
public:Task(){}Task(const int &x, const int &y): _x(x), _y(y){}void operator()(){Excute();}void Excute(){_result = _x + _y;}void ProdectorPrint(){std::cout << "Prodecter->" << std::to_string(_x) + " +" + std::to_string(_y) + " =" + " ?" << std::endl;}void ConsumerPrint(){std::cout << "Conseumer->" << std::to_string(_x) + " +" + std::to_string(_y) + " =" + std::to_string(_result) << std::endl;}std::string Result(){return "Conseumer->" + std::to_string(_x) + " +" + std::to_string(_y) + " =" + std::to_string(_result)+'\n';}std::string Send(){return "Prodecter->" + std::to_string(_x) + " +" + std::to_string(_y) + " =" + " ?"+'\n';}int _x;int _y;int _result;
};//Main.cc
#include <iostream>
#include <memory>
#include <string>
#include <functional>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <vector>
#include <queue>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>#include "task.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
using namespace threadpool;int main()
{// std::unique_ptr<ThreadPool<Task>> tp = std::make_unique<ThreadPool<Task>>();// tp->Init();// tp->Start();int cnt=5;while (cnt--){Task t(1,1);LOG(INFO,t.Send().c_str());ThreadPool<Task>::GetInstance()->Push(t);sleep(1);}ThreadPool<Task>::GetInstance()->Stop();ThreadPool<Task>::GetInstance()->Join();return 0;
}

现象:

为了提高效率,让线程池只能生成一个对象且不支持拷贝,我们使用单例模式来解决:

而单例模式的实现方式有有两种:饿汉实现方式和懒汉实现方式

以洗完的碗为例:

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式

饿汉模式实现:

template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};

懒汉模式实现:

template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
} r
eturn inst;
}
};

以上便是学习多线程的相关知识,有错误欢迎指正~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 程序员纯粹八股文的危害有哪些,应该如何来解决?
  • 为什么 DDoS 攻击偏爱使用 TCP 和 UDP 包?
  • 【常用库】【pytorch】基本部件
  • Spark 基础 与 安装
  • 昇思25天学习打卡营第XX天|RNN实现情感分类
  • Python和java中super的使用用法(有点小语法上的差距,老忘就在这里置顶了)
  • 后端程序员常犯的错误-本地缓存相关bug和技术思考
  • 文心一言 VS 讯飞星火 VS chatgpt (315)-- 算法导论22.3 7题
  • Nginx 高级 扩容与高效
  • 中间件安全:Nginx 解析漏洞测试.
  • CSP 初赛复习 :计算机网络基础
  • java~反射
  • 用Python打造精彩动画与视频,3.2 基本的剪辑和合并操作
  • 【Vulnhub系列】Vulnhub Lampiao-1 靶场渗透(原创)
  • Spring提供的AOP支持是什么
  • [译]CSS 居中(Center)方法大合集
  • 8年软件测试工程师感悟——写给还在迷茫中的朋友
  • AHK 中 = 和 == 等比较运算符的用法
  • Android框架之Volley
  • AngularJS指令开发(1)——参数详解
  • Apache Zeppelin在Apache Trafodion上的可视化
  • Babel配置的不完全指南
  • canvas绘制圆角头像
  • go语言学习初探(一)
  • Java 9 被无情抛弃,Java 8 直接升级到 Java 10!!
  • Java反射-动态类加载和重新加载
  • JDK9: 集成 Jshell 和 Maven 项目.
  • Odoo domain写法及运用
  • Python_网络编程
  • windows-nginx-https-本地配置
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 从零开始的无人驾驶 1
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 讲清楚之javascript作用域
  • 深入浏览器事件循环的本质
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 使用Swoole加速Laravel(正式环境中)
  • const的用法,特别是用在函数前面与后面的区别
  • ​linux启动进程的方式
  • (C++哈希表01)
  • (CVPRW,2024)可学习的提示:遥感领域小样本语义分割
  • (done) 两个矩阵 “相似” 是什么意思?
  • (动手学习深度学习)第13章 计算机视觉---图像增广与微调
  • (附源码)spring boot建达集团公司平台 毕业设计 141538
  • (回溯) LeetCode 131. 分割回文串
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (五)网络优化与超参数选择--九五小庞
  • (原创)Stanford Machine Learning (by Andrew NG) --- (week 9) Anomaly DetectionRecommender Systems...
  • (转)【Hibernate总结系列】使用举例
  • (转)h264中avc和flv数据的解析
  • . Flume面试题
  • ./configure、make、make install 命令
  • .a文件和.so文件
  • .htaccess 强制https 单独排除某个目录