一、线程的概念
1.线程是一个执行分支,执行粒度比进程更细,调度成本更低,线程是CPU调度的基本单位,进程是承担资源分配的基本实体
在linux中进程是由PCB加代码和数据组成,代码和数据需要通过进程维护的地址空间来访问,而进程受CPU控制。在CPU中有各种各样的寄存器,指向进程的PCB,进程地址空间等,除了寄存器还有MMU内存管理单元,计算器,控制器,cache高速缓冲器,其中cache高速缓冲器会在CPU执行一条指令的时候将周围有可能被执行的指令加载到cache高速缓冲器中,达到提高效率的目的,因为高速缓存器的读写速度大于内存的读写速度。
线程的创建,只在进程内部创建PCB,共享父进程的地址空间,假设进程有多个可执行函数,给每个线程对应执行接口,让每个线程都执行一个函数,进程代码的执行就从串行变成了并行,所以线程是一个执行分支,执行粒度比进程更细;
线程以及进程的切换是操作系统维护的,CPU本身无法辨别自己正在切换的是线程还是进程,是操作系统来识别的;当操作系统检测到切换的PCB是线程,就不会切换地址空间了。如果切换进程和切换线程的区别只是用切不切换地址空间来区分,是体现不出来调度成本的缩减的。调度成本的缩减体现在如果切换的是进程,cache高速缓冲器中预加载的有所指令都要替换;如果切换的是线程则不用替换;所以线程的调度成本更低
进程是由PCB以及数据和代码,也有进程地址空间和页表,这些都需要占用系统资源,而线程共享进程的代码和数据,所以进程是承担资源分配的基本实体。进程内部被分为若干个执行流,所以CPU调度的基本单位是线程。
在linux操作系统中没有真正意义上的线程,而是用进程方案模拟的线程;对于线程,操作系统这门学科有统一的理论描述,但是具体到实际的操作系统中去,每个操作系统的实现方式由有所不同,Windows操作系统是单独出创建了TCB结构体来维护线程,这样维护成本很大,而操作系统使用率最高的就是进程,所以Windows系统的长时间运行不是很稳定。而linux的设计者则是看到了线程与进程的相同点,利用进程模拟线程,复制了进程的PCB但是数据和代码不变,所以线程也叫轻量级进程(lwp),线程也有自己的id,可用ps -La查看。
2.物理内存管理
在32位系统下,虚拟地址空间有2的32次方个地址,对标4GB的物理内存空间;虚拟地址的转化需要通过页表,页表属于软件范畴,要描述地址就得是一个结构体,然后实例化初对象来使用,就必须占用空间。一个地址是32位的,想要表示一个地址就必须是一个用一个整型表示。表是一个kv转化模型,而且还附带有权限等其他属性。如果都按4字节算,4,294,967,296121字节=48GB,一个页表的大小就远远超出了内存本身的 大小显然是不可能的。所以一个虚拟地址对应一个字节是不成立的。
虽然寻址的基本单位是1字节,但是内存管理却不是以1字节为单位的。不管是文本文件或者是二进程可执行文件,它们的I/O都需要加载到内存中操作,磁盘比内存的I/O效率低很多,如果按照字节加载势必会被I/O拖慢效率,所以内存到磁盘的I/O是以数据块4KB为单位的,即便是加载到内存的数据比实际被使用的数据要多,也必须以块为单位;预先加载被使用数据周边的数据是局部性原理的特性,可以降低I/O次数提升I/O效率。
文件在磁盘中是以块为单位存储的,内存到磁盘的I/O是以数据块4KB为单位也就要求内存的管理也是以4字节为单位管理的;在磁盘中存储文件内容的数据块叫做页帧,加载到内存中的文件的数据块的存储空间叫做页框;32位计算机内存有4GB,相当于有4GB/4KB=1,048,576个页框。描述内存的结构体在内核中为struct page{},管理page的数据结构为struct page mem[1,048,576]={}。
3.物理地址寻址方式
32位的虚拟地址不是被整体使用的,它被划分为了3段,前两段是10个比特位用于定位页表,最后一段是12个比特位用于定位内存字节空间位置,也叫页内偏移量;页表根据前两段的10个比特位形成了两个级别的页表。一级页表映射第一段的10个比特位,有2的10次方个页表;二级页表映射第二段的10个比特位,也有2的10次方个页表;用第一段比特位找一级页表的key值通过value映射到二级页表,再通过第二段比特位找到二级页表对应的key值,然后通过页表映射到页框;最后通过剩余的12个比特位找到任意字节的位置,12个比特位从全0排列到全1转换成10进制就是0-4095,正好可以表示一个块数据的所有字节;如果一级页表中kv值占用空间20个字节,那么102420/8=2560字节,如果二级页表key值是10个字节,v值是20个字节也就30个字节,一共104857630/8=3,932,160字节,占用的空间是很小的。
4.线程的异常
单个线程出现异常导致线程崩溃,进程也会随着崩溃;比如线程出现CPU计算溢出,或者野指针问题,硬件中断异常等情况都导致进程崩溃。
4.1线程导致进程崩溃试验
这个试验从信号角度讲就是,cpu拿到s的地址进行寻址—>MMU检查访问权限发现改地址处的内容为只读—>MMU发生异常产生硬件断—>向os传递信息—>os向进程发送异常信号终止进程。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* thread_run1(void* args)
{
while(true)
{
sleep(1);
cout<<"i am thread1"<<endl;
}
}
void* thread_run2(void* args)
{
char* s="hello thread";
while(true)
{
sleep(1);
cout<<"i am thread2"<<endl;
*s='H';
}
}
void* thread_run3(void* args)
{
while(true)
{
sleep(1);
cout<<"i am thread3"<<endl;
}
}
int main()
{
pthread_t t1,t2,t3;
pthread_create(&t1,nullptr,thread_run1,nullptr);
pthread_create(&t2,nullptr,thread_run2,nullptr);
pthread_create(&t3,nullptr,thread_run3,nullptr);
while(true)
{
sleep(1);
cout<<"i am main thread"<<endl;
}
return 0;
}
4.线程用途
合理使用多线程,可提高CPU计算密集型程序的执行效率,在多核的情况下可以由多个CPU同时执行计算。
合理使用多线程,可提高io密集型的用户体验,可以一边等待io的执行,一边做其他的事情。
5.线程与进程的共享资源
文件描述符,信号处理函数,当前工作目录,环境变量,用户id和组id;
二、线程的应用
因为Linux没有真正的线程,只有进程模拟的线程;为了方便用户的使用,pthread库封装了轻量级进程的接口,供用户使用;pthread库是Linux系统自带的库,任何系统都会自带这个库,所以这个库也叫做原生库。
1.线程库函数接口介绍
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
功能:在进程内部创建并执行一个线程。
参数:thread线程在返回之前会将id设置到thread中
attr线程属性结构体对象,用来使用设置线程属性的,如果attr设置为空则使用默认属性创建线程
新线程通过调用start_routine()开始执行;
Arg作为start_routine()的第一个参数传递。
返回值:成功返回0,失败返回其他数字,并设置错误码。
int pthread_join(pthread_t thread, void **retval);
功能:等待一个指定的线程,如果线程已经终止,则函数立即返回,被指定的线程必须是可连接的
参数:thread线程id
retval返回值,用来接收线程的退出码,如果线程被pthread_cancel()退出,那么retval的值为PTHREAD_CANCELED。
返回值:成功返回0,失败返回错误编码。
void pthread_exit(void*retval)
功能:终止线程的调用。
参数:retval线程的返回值,可以被主线程的pthread_join的retval接收。
没有返回值
int pthread_cancel(pthread_t thread);
功能:向指定线程发送取消请求
参数:thread线程id
返回值:成功返回0;失败返回错误编码。
pthread_t pthread_self()
功能:返回调用方的线程id
返回值:线程id,总是成功。
int pthread_detach(pthread_t thread);
功能:将一个线程标识为分离状态,被标识为分离的线程在终止后自动释放空间,不需要其他线程连接。
参数:thread线程id
返回值: 成功返回0,失败返回其他错误编码。
int pthread_mutex_init(pthread_mutex_t *restrict(限制),pthread_mutex_t * attr(attribute属性));
功能:初始化互斥锁
参数:restrict互斥锁地址
attribute设置互斥锁属性结构体地址
返回值:成功返回0,否则返回错误码
int pthread_mutex_destroy(pthread_mutex_t* restrict)
功能:删除互斥锁;
参数:restrict互斥锁地址
返回值:成功返回0,否则返回错误码
int pthread_mutex_lock(pthread_mutex_t* mutex)
功能:锁定mutex对象,如果mutex已经被锁定线程会阻塞,知道mutex解锁
参数:mutex互斥量结构体地址
返回值:成功返回0,否则返回错误码
int pthread_mutex_unlock(pthread_mutex_t* mutex)
功能:解锁锁定mutex对象
参数:mutex互斥量结构体地址
返回值:成功返回0,否则返回错误码
2.线程接口使用
2.1所有线程公用地址空间试验
循环创建10个线程,在循环创建线程的同时在堆区开辟一个buffer字符数组,设置buffet的内容为thread从1到10,将buffer空间的地址传递给线程,线程内部接收并打印buffer的内容的都是10,那是因为所有线程拿到的都是同一个buffer的地址空间,在创建完所有的线程后,buffer的内容已经被覆盖成了thread-10,所以所有的线程打印的都是thread-10;
#include <iostream>
#include <pthread.h>
#include<unistd.h>
using namespace std;
int N = 10;
void* thread_run(void* args)
{
char* thname=(char*)args;
while(true)
{
cout<<"i am thread: "<<thname<<endl;
sleep(1);
}
}
int main()
{
pthread_t t;
for (size_t i = 0; i < N; i++)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "thread-%d", i + 1);
pthread_create(&t, nullptr, thread_run, buffer);
}
while(true)
{
cout<<"i am main thread"<<endl;
sleep(1);
}
return 0;
}
将buffer改为每次循环都new一个空间,这样每个线程拿到的地址就是不一样的地址,使用的是不一样的空间,打印的就是不一样的内容了。
#include <iostream>
#include <pthread.h>
#include<unistd.h>
using namespace std;
int N = 10;
void* thread_run(void* args)
{
char* thname=(char*)args;
while(true)
{
cout<<"i am thread: "<<thname<<endl;
sleep(1);
}
}
int main()
{
pthread_t t;
for (size_t i = 0; i < N; i++)
{
char* buffer=new char[64];
snprintf(buffer, 64, "thread-%d", i + 1);
pthread_create(&t, nullptr, thread_run, buffer);
}
while(true)
{
cout<<"i am main thread"<<endl;
sleep(1);
}
return 0;
}
2.2线程通信试验
利用pthread_create可以传参给线程,pthread_exit可以将线程的退出信息返回给pthread_join。利用这些接口特点可以实现线程之间相互通信,主线程可以通过struct对象设置相关信息和一些需要让线程执行的任务传递给线程,线程可以通过args参数获取其他线程传递的信息,同时也可以执行其他线程通过对象传递的任务,然后通过退出接口将执行的结果返回给主线程,主线程通过等待接口拿到执行的结果。
#include <iostream>
#include <pthread.h>
#include<unistd.h>
#include<ctime>
#include<string>
using namespace std;
int N = 10;
class threadData
{
public:
threadData(const string& name,time_t time,int id,int top)
:_name(name)
,_time(time)
,_id(id)
,_top(top)
,_result(0)
{}
public:
//输入
string _name;
uint64_t _time;
int _id;
//返回
int _top;
int _result;
};
void* thread_run(void* args)
{
threadData* dt=static_cast<threadData*>(args);
while(true)
{
cout<<"i am thread,name:"<<dt->_name<<"id:"<<dt->_id<<"create time:"<<dt->_time<<endl;
for(int i=1;i<dt->_top;i++)
{
dt->_result+=i;
}
sleep(10);
pthread_exit((void*)dt);
}
}
int main()
{
pthread_t tid[N];
for (size_t i = 0; i < N; i++)
{
char buffer[1024];
snprintf(buffer, 64, "thread-%d", i + 1);
threadData* dt=new threadData(buffer,time(nullptr),tid[i],100+i);
pthread_create(tid+i, nullptr, thread_run, dt);
}
//等待并回收线程
void *ret=nullptr;
for(int i=0;i<N;i++)
{
pthread_join(tid[i],&ret);
threadData* dt=static_cast<threadData*>(ret);
cout<<dt->_name<<"done "<<"计算结果:"<<dt->_result<<endl;
}
sleep(5);
return 0;
}
2.3线程分离试验
线程的分离是一种属性,被设置分离属性的线程不能被join,不然会出发异常导致程序崩溃;
线程被创建后,那个线程先运行是不确定的,如果直接让被设置分离的线程自己调用pthread_detach那么可能会是主线程先运行pthread_join,这是pthread_n=join已经检查到线程标识不是分离状态了,等到线程再运行完pthread_detach主线程已经不会再检测了,所以不会崩溃;让主线程等待1秒,先被分离的线程运行pthread_detach就会终止了。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
void* pthreadRoutine(void* args)
{
pthread_detach(pthread_self());
int count=5;
while(count)
{
cout<<"i am thread-"<<count--<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,pthreadRoutine,nullptr);
sleep(1);
pthread_join(tid,nullptr);
return 0;
}
三、线程的特性
1.线程库及线程id由来
Linux不存在真正意义的线程,只有轻量级进程。所以Linux就需要原生线程库包装轻量级进程,满足用户对线程的使用需求;pthread库将轻量级进程包装成一个结构体struct pthread。对于线程的操作其实就是对库中结构体的操作,pthread_t对象中存储的就是结构体的起始地址;
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
char* buffer=nullptr;
char* address_shift(pthread_t address)
{
pthread_detach(pthread_self());
buffer=new char();
snprintf(buffer,64,"0x%x",address);
return buffer;
}
void* pthreadRoutine(void* args)
{
sleep(1);
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,pthreadRoutine,nullptr);
cout<<address_shift(pthread_self())<<endl;
free(buffer);
return 0;
}
2.线程独立栈
线程库中封装对于线程的操作有独立的栈空间,这个栈也可以被其他线程使用;在CPU中有edp和esp寄存器控制栈空间的使用范围,CPU只需要切换edp和esp中存储的栈区地址就可以控制栈区的切换,也就完成了线程的独立栈切换;在线程所执行的例程函数中申请的变量空间不共享的,他们存储在不同线程的独立栈中,线程的独立栈是在共享区中的空间。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
int N=3;
void* pthreadRoutine(void* args)
{
int count=3;
while(count)
{
cout<<"thread-"<<count<<"&count: "<<&count<<endl;
count--;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid[N];
for(int i=0;i<N;i++)
{
pthread_create(tid+i,nullptr,pthreadRoutine,nullptr);
}
for(int i=0;i<N;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
3.线程局部存储
在主线程中申请的全局变量对于其他线程来说是共享的,因为全局变量存储在地址空间的已初始化全局数据区,不在栈区中;所以是所以线程共享的。使用__thread 修饰可以使全局数据变为线程局部存储的数据,这个数据存储在共享区中,由库申请。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
int N=3;
int g_val=100;
void* pthreadRoutine(void* args)
{
while(true)
{
cout<<"thread-"<<pthread_self()<<" "<<g_val--<<" "<<"&g_val "<<&g_val<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid[N];
for(int i=0;i<N;i++)
{
pthread_create(tid+i,nullptr,pthreadRoutine,nullptr);
}
for(int i=0;i<N;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
int N=3;
__thread int g_val=100;
void* pthreadRoutine(void* args)
{
while(true)
{
cout<<"thread-"<<pthread_self()<<" "<<g_val--<<" "<<"&g_val "<<&g_val<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid[N];
for(int i=0;i<N;i++)
{
pthread_create(tid+i,nullptr,pthreadRoutine,nullptr);
}
for(int i=0;i<N;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
四、线程互斥
1.互斥锁
如果多个线程同时对一个全局变量做++或者–操作,这个全局变量可能会被这些线程冲突使用;一个++操作在汇编语言汇总起码是要三步操作才能完成,第一步是将数据从内存加载到CPU,第二步CPU内部进行计算,第三步CPU将结果加载回内存,一个线程在进行这三步操作的时候可能会因为时间片到了,被操作系统切换掉,改为其他线程运行,在这之前上一个线程会保存CPU对全局数据的计算进度。其他线程接着操作这个全局数据。在切换到前一个线程的时候,数据已经被其他线程改变了。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
int g_val=10000;
int N=3;
void* pthreadRoutine(void* args)
{
while(true)
{
if(g_val>0)
{
usleep(2000);
cout<<"thread-"<<pthread_self()<<" "<<g_val--<<" "<<"&g_val "<<&g_val<<endl;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t tid[N];
for(int i=0;i<N;i++)
{
pthread_create(tid+i,nullptr,pthreadRoutine,nullptr);
}
for(int i=0;i<N;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
这里的g_val就是临界资源,不对它进行保护(枷锁)就有可能会被线程冲突使用,访问g_val的值的就是临界区;使用线程互斥锁接口函数对临界资源枷锁可以避免这种情况。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;
pthread_mutex_t mutex;
int g_val=10000;
int N=3;
void* pthreadRoutine(void* args)
{
while(true)
{
pthread_mutex_lock(&mutex);
if(g_val>0)
{
usleep(2000);
cout<<"thread-"<<pthread_self()<<" "<<g_val--<<" "<<"&g_val "<<&g_val<<endl;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_init(&mutex,nullptr);
pthread_t tid[N];
for(int i=0;i<N;i++)
{
pthread_create(tid+i,nullptr,pthreadRoutine,nullptr);
}
for(int i=0;i<N;i++)
{
pthread_join(tid[i],nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
2.线程互斥的几点细节
1.凡是访问同一个临界资源的线程,其内部都要多临界区枷锁
2.互斥锁加持的区域不应该有属于非临界区的代码,不然会影响线程的并发执行效率,本来该被执行的代码被封锁了。
3.访问同一个临界资源的临界区代码必须使用同一个互斥锁,不然会影响临界区的访问。
4.互斥体也是临界资源,但是互斥锁的枷锁和解锁是原子的。
3.互斥锁的实现原理
++和–操作在汇编语言中其实是由最少三条指令组成的,所以不具有原子性。互斥体本质是一个存储在内存中的值,加锁的定义中有互换内存中互斥体变量与CPU中控制线程上下文的存储寄存器数据的一条汇编指令,因为是一条汇编指令所以具有原子性。
当操作系统将线程切换给CPU时,线程调用锁,锁向CPU寄存器写0,将mutex中的1切换给CPU的寄存器,将寄存器中的切换给mutex,然后判断寄存器的值是否大于0,大于0则设置锁成功,随即返回。 在执行临界区的时候操作系统切换其他线程给CPU,上一个线程会将寄存器的值拿走。这时候mutex中存储的是0,所以下一个线程中调用的锁再怎么交换CPU的寄存器也是0,只能阻塞等待。
解锁是想mutex中写入1,这个时候其他线程再去执行加锁就能成功了。
4.线程接口封装
#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;
整型数字转字符型数字
char *to_hexadecimal(int address)
{
char *buffer = new char[64];
snprintf(buffer, 64, "0x%X", address);
return buffer;
}
class Thread
{
public:
// 定义状态枚举
typedef enum threadStatus
{
NEW,
RUNNING,
EXIT,
} Status;
// 重定义例程函数指针类型
typedef void (*func_t)(void *);
public:
// 构造,例程函数指针,例程参数
Thread(func_t func, void *args)
: _tid(0),
_status(NEW),
_func(func),
_args(args)
{
char name[64];
snprintf(name, sizeof(name), "thread-%s", to_hexadecimal(pthread_self()));
_name = name;
}
// 返回名字
const string &name() const
{
return _name;
}
// 返回状态
const Status &status() const
{
return _status;
}
// 返回id
const pthread_t &tid() const
{
return _tid;
}
// 运行例程,设置状态
//pthread_create接口的routine参数类型是返回值void*参数为void* 的函数指针
//类成员函数中默认有一个隐藏参数*this,所以需要用进程成员函数
static void* threadRoutine(void*args)
{
//静态成员函数没有this指针,所以无法直接调用类的私有成员属性,所以需要pthread_create传参this
Thread* td=(Thread*)args;
//利用()操作符重载调用例程
(*td)();
}
void operator()()
{
//外部例程可以接收到主线程传递的例程参数
_func(_args);
}
void run()
{
int ret=pthread_create(&_tid, nullptr, threadRoutine, this);
if(ret!=0)exit(1);
_status=RUNNING;
}
// 等待例程
void join()
{
int ret=pthread_join(_tid,nullptr);
if(ret!=0)exit(1);
_status=EXIT;
//如果要使用例程的返回值,可以直接属性_args
}
private:
// 名字
string _name;
// id
pthread_t _tid;
// 状态
Status _status;
// 例程
func_t _func;
// 例程参数
void *_args;
};
main.cc
#include <unistd.h>
#include "thread_package.hpp"
int ticket = 1000;
void routine(void *args)
{
int cnt = 10;
while (cnt--)
{
sleep(1);
std::cout << "thread-" << to_hexadecimal(pthread_self()) << " "
<< "number: " << cnt << std::endl;
}
free(to_hexadecimal(pthread_self()));
}
int main()
{
Thread td1(routine, nullptr);
Thread td2(routine, nullptr);
td1.run();
td2.run();
std::cout << td1.name() << " " << to_hexadecimal(td1.tid()) << " " << td1.status() << std::endl;
std::cout << td2.name() << " " << to_hexadecimal(td1.tid()) << " " << td2.status() << std::endl;
td1.join();
td2.join();
std::cout << td1.name() << " " << to_hexadecimal(td1.tid())<< " " << td1.status() << std::endl;
std::cout << td2.name() << " " << to_hexadecimal(td1.tid())<< " " << td2.status() << std::endl;
return 0;
}
5.互斥锁接口的封装
lockGuard.hpp
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
class Mutex
{
public:
Mutex(pthread_mutex_t*mutex)
:_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~Mutex()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
mythread.cc
#include <unistd.h>
#include"lockGuard.hpp"
int ticket = 1000;
size_t threadN = 4;
// 定义线程数据载体
class tData
{
public:
tData(const char *name, pthread_mutex_t *mutex)
: _name(name), _pmutex(mutex)
{
}
public:
string _name;
pthread_mutex_t *_pmutex;
};
// 定义例程
void *threadRoutine(void *args)
{
tData *td = static_cast<tData *>(args);
while (true)
{
// 枷锁互斥体
{
Mutex m(td->_pmutex);
// 临界区
if (ticket > 0)
{
usleep(2000);
cout << td->_name << " " << ticket-- << endl;
}
else
{
break;
}
}
}
return nullptr;
}
int main()
{
// 创建线程互斥体
pthread_mutex_t mutex;
// 初始化互斥体
pthread_mutex_init(&mutex, nullptr);
// 创建线程id接收对象
pthread_t tid[threadN];
// 创建线程
char buffer[64];
for (int i = 0; i < threadN; i++)
{
snprintf(buffer, sizeof(buffer), "thread-%d", i);
tData *td = new tData(buffer, &mutex);
pthread_create(tid + i, nullptr, threadRoutine, td);
}
// 等待线程
for (int i = 0; i < threadN; i++)
{
pthread_join(tid[i], nullptr);
}
// 主线程
// 删除线程互斥体
pthread_mutex_destroy(&mutex);
return 0;
}
五、线程安全
1.重入函数与线程安全
线程调用可重入函数的时候是暂时安全的,多个线程调用不可重入函数是不安全的,一般不可重入函数中有全局变量或者是静态变量,会发生数据冲突;或者如果调用一个重入函数,这个重入函数只是加锁没有解锁,那么会导致死锁。
2.死锁
两个线程,线程1和线程2,当线程1使用互斥体1加锁,线程2使用互斥体2加锁。线程1在没有释放互斥体1的锁时申请互斥体2的锁,线程2在没有释放互斥体2的锁时申请互斥体1的锁,连个线程就会互相等待对方解锁,但是两个线程又都不解锁就形成了死锁。
一个线程也可能形成死锁,对一个互斥体加锁,不释放的情况下再次申请对同一个互斥体加锁也会形成死锁。
形成死锁的必要条件:1、互斥条件:多线程使用同一个公共资源。2、请求与保持:一个执行流因请求资源被阻塞,造成对以获取的资源不能释放。3、不剥夺条件,一个执行流已获得但没有使用完的情况下,不能强行剥夺。4、循环等待:若干执行流之间形成首尾相接的循环等待。
避免死锁的方法:1、能不加锁就不加锁。2、不同线程保持相同的加锁顺序。3、避免锁不能释放。
六、线程同步
1.同步概念
临界资源的访问都是在加锁之后的,因此如果一个执行流因为没有达到访问临界资源的条件而循环加锁解锁会导致其他执行流很少有机会能访问到临界资源,所以需要保持执行流对临界资源的同步协调使用。
当执行流没有达到临界资源访问的条件时就释放资源然后阻塞等待,不再做无用的资源访问就实现了资源访问同步。
2.条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
1.条件变量接口介绍
int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr);
功能:初始化条件变量
参数:cond条件变量地址,attr用来设置变量属性的结构体地址
返回值:成功返回0,失败返回错误码。
int pthread_cond_destroy(pthread_conde_t* cond);
功能:释放条件变量
参数:cond条件变量地址。
返回值:成功返回0,失败返回错误码。
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex_);
功能:释放锁,并阻塞执行流
参数:cond变量地址,mutex互斥体地址
返回值: 成功返回0,失败返回错误码;
int pthread_cond_signal(pthread_cond_t* cond)
功能:释放一个阻塞变量
返回值:成功返回0,失败返回错误码
2.条件变量接口应用
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int num=5;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void* active(void* args)
{
char*name=static_cast<char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
cout<<name<<": 活动"<<endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid[num];
for(int i=0;i<num;i++)
{
char* name=new char[32];
snprintf(name,32,"thread-%d",i);
pthread_create(tid+i,nullptr,active,name);
}
//释放阻塞队列的第一个变量
while(true)
{
sleep(1);
pthread_cond_signal(&cond);
}
//释放全部变量
/* while(true)
{
sleep(1);
cout<<"main thread wakeup thread"<<endl;
pthread_cond_broadcast(&cond);
} */
for(int i=0;i<num;i++)
{
pthread_join(tid[i],nullptr);
}
return 0;
}
2.1生产者消费者模型
从生活角度理解,生产者是供货商,消费者购物不是直接从供货商哪里购物的,在生产者和消费者之间有超市,生产者可以一次性生产很多商品到超市,超市可以按需将商品零售给消费者。消费者可以不必关心供货商怎么生产,只要超市有商品可买,贡火山也不同关心消费者怎么购买商品,只需要超市有地方可存商品。这样消费者和供货商可以不用步调一致,从而提高了从生产到消费的效率。
这里的超市就是临界资源,生产者和消费者是两个线程,多线程访问临界资源需要维护线程的互斥与同步关系。如果有多个生产者和多个消费者,那么还需维护消费者和消费者之间,生产者和生产者之间的关系。
生产者和生产者之间可能会竞争超市的同一个货架存放商品,所以它们之间是互斥关系。消费者和消费者之间也可能竞争一个商品,所以它们之间也是互斥关系。生产者和消费者之间是同步互斥关系。
2.2生产者消费者模型代码实现
多生产者和多消费者的高效率体现在,一个生产者在生产数据的时候,另一个生产者可能已经在传送数据了,一个消费者在接收数据的时候,另一个消费者可能已经在处理数据了。在生产者接收数据的时候,消费者可以处理已经获取的数据,消费者在处理数据的时候,生产者可以向容器中生产数据;
消费者和消费者之间可以充分利用一个消费者处理数据时不能接收数据的空隙,让其他消费者接收数据。
生产者和生产者之间可以利用一个生产者在接收数据的时候不能传送数据的空隙让其他已经接收到数据的生产者传送数据。
生产者和消费者之间,消费者可以充分利用生产者在接收数据的空隙消费数据,生产者可以利用消费者处理数据的空隙生产数据。
main.cc
#include "queue.hpp"
#include<typeinfo>
// 消费者
void *consumer(void *args)
{
// 接收资源容器对象
blockQueue<Task>* bq=static_cast<blockQueue<Task>*>(args);
while(true)
{
//接收数据
Task t= bq->front();
//删除数据
bq->pop();
//处理数据
t();
//打印处理后的格式化数据
cout<<t.formatResult()<<endl;
}
return nullptr;
}
// 生产者
void *productor(void *args)
{
// 接收队列对象
blockQueue<Task>* bq=static_cast<blockQueue<Task>*>(args);
string op="+-*/%";
while (true)
{
sleep(1);
// 插入数据
int x = rand() % 20 + 1;
int y = rand() % 20 + 1;
char c=op[rand()%op.size()];
Task t(x,y,c);
bq->push(t);
//打印插入后的格式化数据
cout<<t.formatArgs()<<endl;
}
return nullptr;
}
int main()
{
srand((uint64_t)time(nullptr) ^ getpid());
//初始化容器
blockQueue<Task> *bq=new blockQueue<Task>();
pthread_t consumerId[2], productorId[3];
//生产者执行流
pthread_create(&consumerId[0], nullptr, consumer, bq);
pthread_create(&consumerId[1], nullptr, consumer, bq);
//消费者执行流
pthread_create(&productorId[0], nullptr, productor, bq);
pthread_create(&productorId[1], nullptr, productor, bq);
pthread_create(&productorId[2], nullptr, productor, bq);
//连接回收线程
pthread_join(consumerId[0], nullptr);
pthread_join(consumerId[1], nullptr);
pthread_join(productorId[0], nullptr);
pthread_join(productorId[1], nullptr);
pthread_join(productorId[2], nullptr);
delete bq;
return 0;
}
queue.hpp
#pragma once
#include<iostream>
#include<pthread.h>
#include<queue>
#include<string>
#include<time.h>
#include<unistd.h>
#include"task.hpp"
using namespace std;
int number=5;
template <class T>
class blockQueue
{
public:
//判空
bool isEmpty()
{
return _q.empty();
}
//判满
bool isFull()
{
return _q.size()==_capacity;
}
//构造
blockQueue(int capacity=number):_capacity(capacity)
{
//因为只有一个临界资源, 所以只用一把锁
pthread_mutex_init(&_mutex,nullptr);
//因为生产者和消费者使用的资源不同,所以要用到两个条件变量
pthread_cond_init(&_consumerCond,nullptr);
pthread_cond_init(&_productorCond,nullptr);
}
//析构
~blockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_productorCond);
}
//插入数据
void push(const T& val)
{
//加锁
pthread_mutex_lock(&_mutex);
//如果队列满了就不能继续插入数据了,阻塞等待
//cond_wait中有解锁和加锁,当资源使用条件不满足,wait解锁并阻塞等待,
//当wait被唤醒就会重新加锁相互运行,这个时候也可能是多个线程竞争同一个资源,资源
//可能已经被其他线程使用,所以被唤醒后还要继续判断队列是否为满
while(isFull())
{
pthread_cond_wait(&_productorCond,&_mutex);
}
//生产数据
_q.push(val);
//生产者生产一个数据,那么消费者就有至少一个资源,所以这个时候可以唤醒一个消费者
pthread_cond_signal(&_consumerCond);
//解锁
pthread_mutex_unlock(&_mutex);
}
//接收数据
void pop()
{
//加锁
pthread_mutex_lock(&_mutex);
//如果队列空了就不能继续插入数据了,阻塞等待
//cond_wait中有解锁和加锁,当资源使用条件不满足,wait解锁并阻塞等待,
//当wait被唤醒就会重新加锁相互运行,这个时候也可能是多个线程竞争同一个资源,资源
//可能已经被其他线程使用,所以被唤醒后还要继续判断队列是否为空
while(isEmpty())
{
pthread_cond_wait(&_consumerCond,&_mutex);
}
//清除消费的数据
_q.pop();
//消费者消费一个数据,那么生产者就有至少一个资源,所以这个时候可以唤醒一个生产者
pthread_cond_signal(&_productorCond);
pthread_mutex_unlock(&_mutex);
}
T front()
{
//这里也需要加锁和使用条件变量,不然在生产者没有生产之前队列是空的
pthread_mutex_lock(&_mutex);
while(isEmpty())
{
pthread_cond_wait(&_consumerCond,&_mutex);
}
//这里必须使用变量接收返回值,因为临界值使用完不能不解锁。
T val = _q.front();
pthread_mutex_unlock(&_mutex);
return val;
}
private:
queue<T> _q;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _consumerCond;
pthread_cond_t _productorCond;
};
task.hpp
#include <iostream>
#include <string>
using namespace std;
class Task
{
public:
Task()
{}
//构造
Task(int x, int y, char op)
: _x(x), _y(y), _op(op), _result(0), _exitCode(0)
{
}
//处理数据
void operator()()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x / _y;
}
}
break;
case '%':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x % _y;
}
}
break;
default:
break;
}
}
//打印处理之前的数据
string formatArgs()
{
return to_string(_x) + _op + to_string(_y) + '=' + '?';
}
//打印处理之后的数据
string formatResult()
{
return to_string(_x) + _op + to_string(_y) + '=' + to_string(_result);
}
private:
int _x = 0;
int _y = 0;
char _op = 0;
int _result = 0;
int _exitCode = 0;
};
3.信号量
信号量本身就是资源的计数器,pv操作就是对资源的使用和释放是,p操作相当于–,v操作相当于++;++和–操作是不安全的,有可能发生数据冲突,所以pv操作必须是原子的;p操作相当于对资源的预定,即便是现在不使用,将来也可以使用。
3.1信号量结构介绍
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数:sem信号量地址
pshared,为0代表线程共享,为1代表进程共享
value资源数目
int sem_destroy(sem_t *sem);
功能:释放信号量
参数:sem信号量地址
int sem_post(sem_t *sem);
功能:p操作
参数:sem信号量地址
int sem_wait(sem_t *sem);
功能:v操作
参数:sem信号量地址
返回值:成功返回0,失败返回错误码
3.2信号量实现循环队列生产者消费者模型
循环队列的概念
一个首位相接的数组,front和back指向一个位置的时候是空,back指向front的前一个位置是满。back永远指向最后一个数据的下一个位置,也就是空,插入数据是back位置插入后back再往后移动,front永远指向队头,删除数据是删除front位置的数据然后front更新到下一个位置。
循环队列做生产者消费者模型的通信容器
生产者与消费者之间需要维护同步与互斥的关系;当生产者和消费者访问的是队列中不同位置的资源时,它们可以同时访问;只有队列是空,数据为满后back再次将插入一个数据的时候生产者和消费者会同时 访问一个空间,所以只需要防止者这两种情况出现,其他情况都可以并发访问。
一次p操作相当于对一个队列资源的预定,v操作相当于对资源的释放;但是生产者和消费者看待的资源是不一样的,生产者关注的是队列还有没有空间,消费者关心的是队列中还有没有数据。
当队列为满时就先让消费者运行,队列为空时就先让生产者运行。
代码实现
task.hpp
#include <iostream>
#include <string>
using namespace std;
class Task
{
public:
Task()
{}
//构造
Task(int x, int y, char op)
: _x(x), _y(y), _op(op), _result(0), _exitCode(0)
{}
//处理数据
void operator()()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x / _y;
}
}
break;
case '%':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x % _y;
}
}
break;
default:
break;
}
}
//打印处理之前的数据
string formatArgs()
{
return to_string(_x) + _op + to_string(_y) + '=' + '?';
}
//打印处理之后的数据
string formatResult()
{
return to_string(_x) + _op + to_string(_y) + '=' + to_string(_result)+"(" +to_string(_exitCode)+" )";
}
private:
int _x = 0;
int _y = 0;
char _op = 0;
int _result = 0;
int _exitCode = 0;
};
ringqueue.hpp
#include<iostream>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
using namespace std;
static const int n=5;
template <class T>
class ringQueue
{
private:
//p操作封装
void p(sem_t& sem)
{
sem_wait(&sem);
}
//v操作封装
void v(sem_t& sem)
{
sem_post(&sem);
}
//加锁封装
void lock(pthread_mutex_t& mutex)
{
pthread_mutex_lock(&mutex);
}
//解锁封装
void unlock(pthread_mutex_t& mutex)
{
pthread_mutex_unlock(&mutex);
}
public:
//构造
ringQueue():_capacity(n),_c_step(0),_p_step(0),_container(_capacity)
{
//初始化空间和资源信号量
sem_init(&_space_sem,0,_capacity);
sem_init(&_data_sem,0,0);
//初始化消费者和生产者互斥锁
pthread_mutex_init(&_c_mutex,nullptr);
pthread_mutex_init(&_p_mutex,nullptr);
}
//生产
void push(const T& data)
{
//申请空间信号量
p(_space_sem);
//加锁
lock(_p_mutex);
//插入数据
_container[_p_step++]=data;
//将访问下标控制在环形区域内
_p_step%=_capacity;
//解锁
unlock(_p_mutex);
//释放资源信号量
v(_data_sem);
}
//消费
void pop(T* ret)
{
//申请资源信号量
p(_data_sem);
//加锁
lock(_c_mutex);
//将资源输出给消费者
*ret=_container[_c_step++];
//控制下标
_c_step%=_capacity;
//解锁
unlock(_c_mutex);
//释放空间信号量
v(_space_sem);
}
//析构
~ringQueue()
{
//释放锁和信号量
sem_destroy(&_data_sem);
sem_destroy(&_space_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
//环形队列容量
int _capacity;
//消费者访问位置
int _c_step;
//生产者访问位置
int _p_step;
//容器
vector<T> _container;
//空间信号量
sem_t _space_sem;
//数据信号量
sem_t _data_sem;
//消费者锁
pthread_mutex_t _c_mutex;
//生产者锁
pthread_mutex_t _p_mutex;
};
main.cc
#include "ringqueue.hpp"
#include "task.hpp"
#include <time.h>
#include<string.h>
const char* str="+-*/%";
static const int N = 3;
// 消费者例程
void* consumerRoutine(void* argc)
{
//接收循环队列对象
ringQueue<Task>* rq=static_cast<ringQueue<Task>*>(argc);
while(true)
{
//用输出型参数接收数据
Task t;
rq->pop(&t);
//处理数据
t();
//打印数据处理结果
cout<<"consumer: "<<t.formatResult()<<endl;
}
return nullptr;
}
// 生产者例程
void* productorRoutine(void* argc)
{
ringQueue<Task>* rq=static_cast<ringQueue<Task>*>(argc);
while(true)
{
//生产数据
int x=rand()%100;
int y =rand()%100;
char op=str[rand()%strlen(str)];
Task t(x,y,op);
//插入循环队列
rq->push(t);
//打印插入的数据
cout<<"productor: "<<t.formatArgs()<<endl;
}
return nullptr;
}
int main()
{
// 环形队列对象
ringQueue<Task> *rq = new ringQueue<Task>;
// 线程对象
pthread_t productor[N], consumer[N];
// 创建并运行消费者生产者线程
for (int i = 0; i < N; i++)
pthread_create(productor + i, nullptr, productorRoutine, rq);
for (int i = 0; i < N; i++)
pthread_create(consumer + i, nullptr, consumerRoutine, rq);
// 连接所有线程
for (int i = 0; i < N; i++)
{
pthread_join(productor[i], nullptr);
pthread_join(consumer[i], nullptr);
}
delete rq;
// 释放环形队列对象,以及线程对象
return 0;
}
七、线程池
1.概念
线程池是一种使用模式,线程如果被过多的调用会影响性能,线程池维护了有限多个线程,这些线程可以并发的执行任务,当任务过多的时候,任务会在任务队列中阻塞,而不会过度的创建线程.线程池被一个单例模式的类封装,全局只有一个对象,程序完全结束的时候释放,避免了短时间内创建和销毁线程的开销.
2.编程思想
这个类是单例模式的类,构造函数时私有化的,定义一个静态变量初始化为空,接收用静态成员函数创建的类对象指针.静态成员函数只创建一次/一个对象.如果是多线程使用这个单例类,还要给这个静态对象加锁,防止对象被重复创建.
有任务插入和任务提取成员函数,用来接收任务插入到任务容器中,或者是将任务提取出俩给线程处理.插入和提取都需要操作任务队列,stl容器不是线程安全的,所以要加锁,为了防止插入和提取饥饿问题还要使用条件变量.
锁的使用,为了防止在临界区出现异常跳出当前作用域,造成加锁没有解锁的情况,也就是死锁问题,使用封装的锁,lockGuard类,这个类封装了mutex对象的加锁和解锁,创建对象自动加锁,出了对象所在作用域会自动解锁,lockGurad不能封装mutex的初始化和析构,不然使用lockGuard对象使用完一次就会将_mutex成员释放,造成对象后面使用时为随机值.
线程容器内存储的是封装的线程类,这个线程类可以预先接收线程例程和参数,然后再运行,所以线程的分为初始化和运行两个成员函数处理,线程在运行时,使用范围for必须使用引用传参,不然运行的是拷贝后的线程类对象,会造成段错误.
这些线程是并发交替运行的,谁先抢到资源,谁就先处理资源,如果没有抢到资源就会被条件变量阻塞等待,直到被唤醒.
3.代码实现
mutexpackage.hpp
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
class lockGuard
{
public:
lockGuard(pthread_mutex_t* mutex):_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~lockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t* _mutex;
};
threadpackage.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;
//整型数字转字符型数字
char *to_hexadecimal(int address)
{
char *buffer = new char[64];
snprintf(buffer, 64, "0x%X", address);
return buffer;
}
class Thread
{
public:
// 定义状态枚举
typedef enum threadStatus
{
NEW,
RUNNING,
EXIT,
} Status;
// 重定义例程函数指针类型
typedef void (*func_t)(void *);
public:
// 构造,例程函数指针,例程参数
Thread(func_t func, void *args)
: _tid(0),
_status(NEW),
_func(func),
_args(args)
{
char name[64];
snprintf(name, sizeof(name), "thread-%s", to_hexadecimal(pthread_self()));
_name = name;
}
// 返回名字
const string &name() const
{
return _name;
}
// 返回状态
const Status &status() const
{
return _status;
}
// 返回id
const pthread_t &tid() const
{
return _tid;
}
// 运行例程,设置状态
//pthread_create接口的routine参数类型是返回值void*参数为void* 的函数指针
//类成员函数中默认有一个隐藏参数*this,所以需要用进程成员函数
static void* threadRoutine(void*args)
{
//静态成员函数没有this指针,所以无法直接调用类的私有成员属性,所以需要pthread_create传参this
Thread* td=(Thread*)args;
//利用()操作符重载调用例程
(*td)();
}
void operator()()
{
//外部例程可以接收到主线程传递的例程参数
_func(_args);
}
void run()
{
int ret=pthread_create(&_tid, nullptr, threadRoutine, this);
if(ret!=0)exit(1);
_status=RUNNING;
}
// 等待例程
void join()
{
int ret=pthread_join(_tid,nullptr);
if(ret!=0)exit(1);
_status=EXIT;
//如果要使用例程的返回值,可以直接属性_args
}
private:
// 名字
string _name;
// id
pthread_t _tid;
// 状态
Status _status;
// 例程
func_t _func;
// 例程参数
void *_args;
};
task.hpp
#pragma once
#include <iostream>
#include <string>
using namespace std;
class Task
{
public:
Task()
{}
//构造
Task(int x, int y, char op)
: _x(x), _y(y), _op(op), _result(0), _exitCode(0)
{}
//处理数据
void operator()()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x / _y;
}
}
break;
case '%':
{
if (_y == 0)
{
_exitCode = -2;
}
else
{
_result = _x % _y;
}
}
break;
default:
break;
}
}
//打印处理之前的数据
string formatArgs()
{
return to_string(_x) + _op + to_string(_y) + '=' + '?';
}
//打印处理之后的数据
string formatResult()
{
return to_string(_x) + _op + to_string(_y) + '=' + to_string(_result)+"(" +to_string(_exitCode)+" )";
}
private:
int _x = 0;
int _y = 0;
char _op = 0;
int _result = 0;
int _exitCode = 0;
};
threadpool.hpp
#pragma once
#include "mutexpackage.hpp"
#include "task.hpp"
#include "threadpackage.hpp"
#include <vector>
#include <queue>
using namespace std;
template <class T>
class threadpool
{
// 静态对象指针
static threadpool<T> *stp;
public:
// 静态对象指针初始化并返回
static threadpool<T> *getInstance()
{
// 如果stp不为空就没必要加锁了
// stp的值不会被更改,所以是安全的
if (stp == nullptr)
{
{
// 需要改变stp的值,所以需要加锁
// mutex保护的是任务容器,这里是stp
// 不是一个临界资源,所以另起一把锁
lockGuard ld(&_instance_mutex);
if (stp == nullptr)
{
// 在对上创建对象
stp = new threadpool<T>;
// 初始化,运行线程
stp->init();
stp->start();
}
}
}
return stp;
}
// 条件变量阻塞封装
void wait()
{
pthread_cond_wait(&_cond, &_mutex);
}
// 条件变量唤醒封装
void wake_up()
{
pthread_cond_signal(&_cond);
}
// 任务队列判空封装
bool isEmpty()
{
return _task.empty();
}
// 任务插入
void push(const T &task)
{
lockGuard ld(&_mutex);
_task.push(task);
wake_up();
}
// 任务提取
T pop()
{
T t = _task.front();
_task.pop();
return t;
}
// 线程例程
static void threadRoutine(void *args)
{
threadpool<T> *tp = static_cast<threadpool<T> *>(args);
while (true)
{
T t;
{
// 获取任务时加锁
lockGuard ld(&tp->_mutex);
// 判断是否有资源
while (tp->isEmpty())
{
// 没有资源就等待
tp->wait();
}
// 提取资源
t = tp->pop();
}
// 处理资源
t();
// 打印初始结果
cout << "thread routine: " << t.formatResult() << endl;
}
}
// 线程初始化
void init()
{
for (int i = 0; i < _capacity; i++)
{
_thread.push_back(Thread(threadRoutine, this));
}
}
// 线程运行
void start()
{
for (auto &t : _thread)
{
t.run();
}
}
~threadpool()
{
for (auto &t : _thread)
{
t.join();
}
pthread_cond_destroy(&_cond);
pthread_mutex_destroy(&_mutex);
}
private:
// 构造私有化
threadpool(int capacity = 5)
: _capacity(capacity)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_cond, nullptr);
}
threadpool(const threadpool<T> &tp) = delete;
threadpool<T>& operator=(const threadpool<T> &tp) = delete;
private:
// 线程池
vector<Thread> _thread;
// 任务容器
queue<T> _task;
// 线程池容量
int _capacity;
// 任务容器锁
pthread_mutex_t _mutex;
// 任务条件变量
pthread_cond_t _cond;
// 静态对象锁
static pthread_mutex_t _instance_mutex;
};
// 静态对象指针初始化
template <class T>
threadpool<T> *threadpool<T>::stp = nullptr;
// 静态对象锁初始化
template <class T>
pthread_mutex_t threadpool<T>::_instance_mutex = PTHREAD_MUTEX_INITIALIZER;
main.cc
#include"threadpool.hpp"
#include<unistd.h>
#include<time.h>
#include<string.h>
const char* str="+-*/%";
int main()
{
srand(time(nullptr));
while(true)
{
int x=rand()%100;
int y=rand()%100;
char op=str[rand()%strlen(str)];
Task t(x,y,op);
//使用时才会创建对象
cout<<"usre: "<<t.formatArgs()<<endl;
threadpool<Task>::getInstance()->push(t);
}
}
八、各种锁
1.常见的几种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁::不断判断资源是否就位,不断加解锁.
2.自旋锁
当线程在临界区的时间比较短时可使用自旋锁,大概率不会造成饥饿问题,但是不适合在临界区的时间比较长的情况.
自旋锁接口介绍
初始化,释放
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
加锁,解锁
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
3.读写锁
读写者问题跟生产这消费者有相似之处,不同的是消费者和消费者需要维护互斥关系,因为消费者是要将数据拿走的,但是读者和读者之间不用维护互斥关系,因为读者不会改变数据.
接口介绍
设置读写者优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
初始化和释放
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
读写者加锁原理
起初是读者和写者竞争信号量,如果是写者首先拥有信号量,要先判断是否有读者在读信息,如果有就释放信号量并退出,如果没有就写入;如果是读者首先拥有信号,就对读者计数器做加加操作,读完毕后再做减减操作.,这两步操作都需要加锁.因为读者和读者之间是共享资源的.
伪代码
pthread_mutex_t mutex;
sem_t w(1);
int read_count=0;
读者
pthread_mutex_lock(&mutex)
if(read_count==0)p(w);
read_count++;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
...................读操作
read_cont--;
pthread_mutex_unlock(&mutex);
写者
p(w);
if(read_count>0)
{
v(w);
return;
}
...........写操作
v(w);