目录
一. 概念
1.概念
在我们之前所学到的进程中,只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和数据,是操作系统调度的基本单位。
由于每个线程都运行在进程中,因此,一个进程可以包含多个线程,因此,我们依旧需要将线程进行先描述,再组织,而描述的过程需要使用到TCB结构体。
而在Linux中,并没有真正意义上的线程结构(TCB),而是由进程PCB模拟出来的。进程包括程序、task_struct(PCB)、mm_struct(进程地址空间)、页表和mmu。而Linux通过复用PCB,使得多个task_struct共用同一个进程地址空间、页表和mmu。
其中,各个线程的资源大部分都是共享的,例如文件描述符表、每种信号的处理方式、当前工作目录、用户id和组id以及进程地址空间中的代码区、已初始化数据区、未初始化数据区、堆区(各个线程申请的空间一般只会由自己使用,可以认为是私有的)、共享区。而除此之外,线程ID、一组寄存器(线程的上下文)、栈区(涉及到函数的调用需要入栈、出栈操作,临时变量也需要存储在栈区)、errno、信号屏蔽字、调度优先级是独自占用的。
而进程作为分配系统资源的基本实体,CPU将当前进程的资源划分给不同的task_struct。
正因如此,Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口,即操作系统在用户层实现了一套用户层多线程方案,以库的方式提供给用户使用,即phread线程库。
2.线程库
pthread_create 函数
#include<pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread:指向线程标识符的指针,用于存储新创建的线程的ID。(线程的栈结构由pthread库来维护,而线程的ID指的是该线程在库中所占结构体的起始地址)
attr:指向线程属性的指针,用于指定新线程的属性。可以为NULL,表示使用默认属性。
start_routine:指向线程函数的指针,新线程会从该函数开始执行。
arg:传递给线程函数(start_routine)的参数。
线程创建成功返回0 失败返回错误码
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* threadRun(void* args){
const string name=(char*)args;
cout<<name<<", pid:"<<getpid()<<endl;
while(true);
}
int main(){
pthread_t tid;
char name[64]="thread ";
for(int i=0;i<5;i++)
{
name[7]=i+'0';
pthread_create(&tid, nullptr, threadRun, (void*)(name));
sleep(1);
}
cout<<"main, pid:"<<getpid()<<endl;
while(true);
return 0;
}
要注意的是,在我们编译时,要使用 -l 选项标识库名。
我们可以使用ps -aL来查看线程
LWP为线程的编号,其中主线程的编号与当前进程的PID相同。
而当一个线程出现异常后,无论其他线程是否退出,该进程都会退出
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* threadRun(void* args){
const string name=(char*)args;
cout<<name<<", pid:"<<getpid()<<endl;
sleep(2);
int a=10;
a/=0;
}
int main(){
pthread_t tid;
char* name="thread 1";
pthread_create(&tid, nullptr, threadRun, (void*)name);
cout<<"main, pid:"<<getpid()<<endl;
while(true);
return 0;
}
pthread_join 函数
线程在创建并执行的时候,线程也是需要进行等待的,如果主线程不等待,会引起类似于进程中的僵尸进程的问题,导致内存泄漏。
#include<pthread.h>
int pthread_join(pthread_t thread, void** retcval);
thread:要等待的线程的标识符,即要等待的子线程
retcval:用于接收被等待进程的退出状态或返回值(指向子线程函数的返回值),可为空
若成功,则返回0,负责,返回错误代码。
相较于进程而言,线程的等待只能是阻塞等待。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* threadRun(void* args){
const string name=(char*)args;
sleep(2);
cout<<name<<", pid:"<<getpid()<<endl;
char* ret="child thread ret str";
return (void*)ret;
}
int main(){
pthread_t tid;
char* name="thread 1";
char* thread_ret;
pthread_create(&tid, nullptr, threadRun, (void*)name);
pthread_join(tid, (void**)&thread_ret);
cout<<"main, pid:"<<getpid()<<endl;
cout<<"thread ret: "<<thread_ret<<endl;
return 0;
}
pthread_exit 函数
#include<pthread.h>
void pthread_exit(void *retval);
线程退出,使用方法类似于exit函数,与return等价。
pthread_cancel 函数
#include<pthread.h>
int pthread_cancel(pthread_t thread);
成功返回0,失败返回非0值
当线程被 pthread_cancel 函数取消后,再进行pthread_join时,退出码为-1(PTHREAD_CANCLELED)
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using namespace std;
void* threadRun(void* args){
const string name=(char*)args;
cout<<name<<", pid: "<<getpid()<<endl;
while(true){
cout<<name<<endl;
sleep(1);
}
}
int main(){
pthread_t tid;
char* name="thread 1";
pthread_create(&tid, nullptr, threadRun, (void*)name);
sleep(2);
pthread_cancel(tid);
int thread_ret=0;
pthread_join(tid, (void**)&thread_ret);
cout<<"main, pid: "<<getpid()<<endl;
cout<<"thread ret: "<<thread_ret<<endl;
return 0;
}
pthread_self 函数
#include<pthread.h>
pthread_t pthread_self();
获取当前进程ID
pthread_detach 函数
#include<pthread.h>
int pthread_detach(pthread_t thread);
线程分离
__pthread 关键字
修饰全局变量,使得每个线程独占一个该全局变量
3.线程优缺点
优点
- 创建一个新线程的代价要比创建一个新进程小得多,且线程占用的资源要比进程少很多(只需要创建TCB)。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(不需要切换进程地址空间和页表,更重要的是不需要切换缓存)。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。、
高速缓存简介
一个程序的机器指令最初是存放在磁盘上的,当程序加载时,它们被复制到主存;当处理器运行程序时,指令又从主存复制到处理器。
根据机械原理,较大的存储设备要比较小的存储设备运行得慢,而一个典型的寄存器文件只存储几百字节的信息,而主存中可存放几十亿字节,因此,处理器从寄存器文件中读数据比从主存中读取几乎要快一百倍。
针对这种处理器与主存之前的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory),作为暂时的集结区域,存放处理器近期可能会需要的信息。
缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
二. 线程互斥
1.相关概念
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
例如这样一段代码
#include<iostream>
using namespace std;
#include<pthread.h>
#include <unistd.h>
int ticket=20;
void* threadRun(void* args){
char* name=(char*)args;
while(true)
{
if(ticket>0)
{
usleep(1000);
printf("%s sells ticket:%d\n", name, ticket);
ticket--;
}
else
break;
}
}
int main(){
pthread_t pid1, pid2, pid3;
pthread_create(&pid1, nullptr, threadRun, (void*)"thread 1");
pthread_create(&pid2, nullptr, threadRun, (void*)"thread 2");
pthread_create(&pid3, nullptr, threadRun, (void*)"thread 3");
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
pthread_join(pid3, NULL);
return 0;
}
其中,ticket是临界资源,而 threadRun 中 if 中的语句为临界条件。
默认状态下,并不会处于互斥状态,因此会出现一系列问题
例如,在进入if语句后,在ticket进行 -- 操作之前,若是进行了线程切换,那么,ticket会被执行多次 -- 操作,可能会使得在多次printf时ticket相同,或是在ticket小于等于0时依旧进行 -- 操作。
因此,我们需要依赖互斥锁(互斥量)来实现线程的互斥。
2.互斥锁
创建、销毁
初始化互斥量有两种方法:分为静态分配和动态分配
在进行静态分配时,只需要通过宏来进行初始化
#include<pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
而在进行动态分配时,则需要调用对应的函数
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:需要被初始化的互斥锁对象,它是一个指向pthread_mutex_t类型的指针。
attr:用来指定互斥锁的属性,通常可以设置为NULL,表示使用默认属性。
函数执行成功时返回0,否则返回错误码。
而当一个动态分配的锁不处于加锁状态并且不再需要使用这个锁,那么我们需要调用对应函数将其销毁
#include<pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
如果成功销毁互斥锁,函数返回0;如果发生错误,返回一个非零的错误码。
加锁、解锁
我们需要在临界区前加锁、临界区后解锁,以实现线程互斥
#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
#include<iostream>
using namespace std;
#include<pthread.h>
#include <unistd.h>
int ticket=20;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* threadRun(void* args){
char* name=(char*)args;
while(true)
{
pthread_mutex_lock(&mutex);
if(ticket>0)
{
usleep(1000);
printf("%s sells ticket:%d\n", name, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main(){
pthread_t pid1, pid2, pid3;
pthread_create(&pid1, nullptr, threadRun, (void*)"thread 1");
pthread_create(&pid2, nullptr, threadRun, (void*)"thread 2");
pthread_create(&pid3, nullptr, threadRun, (void*)"thread 3");
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
pthread_join(pid3, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
这样,我们就可以实现线程的互斥。
而由于具有原子性的代码只能同时由一个线程执行,效率较慢,因此在加锁与解锁的过程,我们需要保证加锁和解锁尽量靠近临时区,保证加锁的粒度越小越好。
底层实现
在加锁的过程中,我们首先向al寄存器(进程的上下文)赋值0,当线程被切换时,al 的值也会跟着线程被切换。
之后,我们通过一条汇编指令xchgb来将寄存器al的值与mutex的值进行交换,使得al寄存器中的值大于0,而mutex的值为0。正因如此,一个线程在加锁的过程中无论之前是否被切换,当它执行到该条指令时,该线程的上下文al就会变为大于零的值,而mutex变为0,会去执行if内部命令。而当解锁之前切换到其他线程时,此时al与mutex都为0,这也就使得交换没有意义,会去执行else内部命令。
而在解锁时,只需要将mutex重新置为 1 即可。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁
- 破坏死锁的四个必要条件。
- 加锁顺序一致。
- 避免锁未释放的场景。
- 资源一次性分配。
3.可重入、线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
线程安全
线程安全情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作。
- 多个线程之间的切换不会导致该接口的执行结果存在二义性。
线程不安全情况
- 不保护共享变量的函数。
- 函数状态随着被调用,状态发生变化的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
重入
可重入情况
- 不使用全局变量或静态变量。
- 不使用用malloc或者new开辟出的空间。
- 不调用不可重入函数。
- 不返回静态或全局数据,所有数据都有函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
不可重入情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
- 可重入函数体内使用了静态的数据结构
可重入函数一定是线程安全的,而线程安全的函数不一定是可重入函数。
三. 线程同步
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。而若是线程频繁得进行访问,不仅会浪费双方的资源,也会造成其他进程的饥饿问题。
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
1.生产者消费者模型
生产者消费者模型中包含两种角色:生产者(线程)和消费者(线程),其中,生产者是在该模型中的容器(缓冲区)中添加数据,而消费者是在该模型中的容器中获取数据。
其中,生产者与生产者之间在添加数据的过程中是互斥的,而在添加数据之前,需要先产生相应的数据,因此,此时这些生产者是并发的。同样,消费者与消费者之间在获取数据的过程中是互斥的,而在获取数据之后,需要对所获取到的数据进行处理,此时这些消费者也是并发的。
而若是生产者添加数据时所对应的容器位置与消费者获取数据所对应的容器位置相同,它们之间的关系是互斥的,而其他时候它们之间是并发的。
2.条件变量
为了实现线程同步,其中一种方法是使用环境变量
当我们处理临界资源前,要对临界资源是否符合条件进行检测,检测的过程也属于临界区,因此我们需要频繁地进行加锁和解锁。而条件变量可以解决这个问题。
创建与销毁
#include<pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond)
等待与唤醒
#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
其中,当代码执行到 pthread_cond_wait 函数时,会进行阻塞等待,根据时间前后线程以队列的形式进行等待,直到条件变量被唤醒,其中,pthread_cond_broadcast 函数使得所有等待的线程都向下执行,而 pthread_cond_signal 函数使得队列头部的线程向下执行。需要注意的是,当我们因某些条件不满足而进行等待时,有可能会出现伪唤醒或是函数调用失败的问题,因此我们需要再次进行对条件的检测。
而为了避免在加锁的过程中等待而导致其他线程的临界区代码无法正常执行,pthread_cond_wait 函数的第二个参数restrict mutex表示在等待前自动释放该锁,当该条件变量被唤醒后,又会重新进行加锁。
#include<iostream>
using namespace std;
#include<pthread.h>
#include<string>
#include<unistd.h>
typedef void (*func_t)(const string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond);
class ThreadData
{
public:
ThreadData(const string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
:_name(name), _func(func), _pmtx(pmtx), _pcond(pcond)
{ }
public:
string _name;
func_t _func;
pthread_mutex_t *_pmtx;
pthread_cond_t *_pcond;
};
void func(const std::string &name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(true)
{
pthread_cond_wait(pcond, pmtx);
cout<<name<<" running"<<endl;
}
}
void* entry(void *args)
{
ThreadData* td=(ThreadData*)args;
td->_func(td->_name, td->_pmtx, td->_pcond);
delete(td);
}
int main()
{
pthread_t tids[4];
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
pthread_cond_t cond;
pthread_cond_init(&cond, nullptr);
//线程创建
for(int i=0;i<4;i++)
{
string name="thread ";
name+=i+'1';
ThreadData* args=new ThreadData(name, func, &mutex, &cond);
pthread_create(tids+i, nullptr, entry, (void*)args);
}
//条件变量唤醒
sleep(2);
int count=10;
while(--count>=0)
{
cout<<"resume thread run code ...."<<endl;
pthread_cond_broadcast(&cond);
//pthread_cond_signal(&cond);
sleep(1);
}
//线程等待
for(int i=0;i<4;i++)
{
pthread_join(tids[i], nullptr);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
当我们使用 pthread_cond_broadcast 函数时
也正是因为线程是以队列的形式等待,因此每次唤醒线程的执行顺序都是相同的
当我们使用 pthread_cond_signal 函数时
因此,当我们涉及到临界资源时,在不符合条件的情况下,我们可以进行条件变量的等待,当临界资源就绪后,再进行唤醒,以减少资源的消耗。
阻塞队列
阻塞队列有一定的容量,若是消费者从该队列中获取数据的时候队列中没有数据或是生产者从该队列中添加数据队列已满,这时线程需要进行阻塞等待,而为了避免多次访问队列(临界资源),就可以使用条件变量来实现同步。
#include <iostream>
using namespace std;
#include <pthread.h>
#include <mutex>
#include <queue>
#include <unistd.h>
#include <ctime>
const int defaltCapacity=5;
template <class T>
class BlockQueue
{
public:
BlockQueue(int capacity=defaltCapacity)
:_capacity(capacity)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_Empty, nullptr);
pthread_cond_init(&_Full, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_Empty);
pthread_cond_destroy(&_Full);
}
void push(const T &in)
{
pthread_mutex_lock(&_mtx);
while(que.size()==_capacity)
pthread_cond_wait(&_Full, &_mtx);
que.push(in);
pthread_cond_signal(&_Empty);
pthread_mutex_unlock(&_mtx);
}
void pop(T& out)
{
pthread_mutex_lock(&_mtx);
while(que.empty())
pthread_cond_wait(&_Empty, &_mtx);
out=que.front();
que.pop();
pthread_cond_signal(&_Full);
pthread_mutex_unlock(&_mtx);
}
private:
queue<T> que;
int _capacity;
pthread_mutex_t _mtx;
pthread_cond_t _Empty;
pthread_cond_t _Full;
};
void* consumer(void* args)
{
BlockQueue<int>* bq=(BlockQueue<int>*) args;
while(true)
{
int out=0;
bq->pop(out);
printf("consumer pop: %d\n", out);
}
}
void* productor(void* args)
{
BlockQueue<int>* bq=(BlockQueue<int>*) args;
while(true)
{
int in=rand()%100;
bq->push(in);
printf("productor push: %d\n", in);
sleep(2);
}
}
int main()
{
srand((unsigned int)time(NULL));
BlockQueue<int>* bq=new BlockQueue<int>();
pthread_t cpid[2], ppid[2];
pthread_create(cpid, nullptr, consumer, bq);
pthread_create(cpid+1, nullptr, consumer, bq);
pthread_create(ppid, nullptr, productor, bq);
pthread_create(ppid+1, nullptr, productor, bq);
pthread_join(cpid[0], nullptr);
pthread_join(cpid[1], nullptr);
pthread_join(ppid[0], nullptr);
pthread_join(ppid[1], nullptr);
delete bq;
return 0;
}
当生产者生产数据的速度远大于消费者获取数据的速度时
首先,生产者先添加数据进入阻塞队列,直到被填满,之后进入阻塞等待。每当消费者获取一个数据后,阻塞队列不为满,条件变量 _Full 激活,生产者便添加一个数据后继续阻塞等待。
而当生产者生产数据的速度远小于消费者获取数据的速度时
消费者会进行阻塞等待,每当生产者添加一个数据进入阻塞队列, 阻塞队列不为空,条件变量 _Empty 激活,消费者获取一个数据后继续阻塞等待。
3.POSIX信号量
实现线程同步的另外一种方法是使用POSIX信号量。
POSIX信号量和SystemV信号量相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但POSIX可以用于线程间同步。
创建与销毁
#include<semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_sestory(sem_t *sem);
pshared:0表示线程间共享,非0表示进程间共享
value:信号量初始值
等待与发布
#include<semphore.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
等待:信号量的值减1
发布:信号量的值加1
POSIX信号量本质上就是一种加减操作具有原子性的计数器,当进行信号量的等待时信号量的值为0,那么就进行阻塞等待,直到信号量不为0。
与条件变量不同的是,POSIX信号量是通过对非临界资源(信号量本身的数值)的访问来判断是否需要进行阻塞等待,因此,更加减少了对临界资源的访问。
环形队列
我们可以通过对下标进行模运算来使用数组模拟环形队列。我们需要使用到两个下标表示队列已存储区域的头和尾。当这两个下标相同,则队列为空,而当尾下标的下一个位置为头下标,那么表示队列为满。
在生产者与消费者之间,当这两个下标处于同一个位置(队列为空),那么该下标所指向的内容为临界资源,因此我们可以实现同步来减少对临界资源的访问。而当两个下标处于不同位置(队列不为空),那么生产者与消费者可以实现并发。
而生产者与生产者之间、消费者与消费者之间所使用的下标永远都是同一个,因此我们我们可以使用两把锁来实现互斥。
#include<iostream>
using namespace std;
#include<pthread.h>
#include<semaphore.h>
#include<vector>
# include <unistd.h>
#include<ctime>
const int rq_capacity=5;
template <class T>
class RingQueue
{
public:
RingQueue(int cap=rq_capacity)
:_capacity(cap)
{
rq.resize(cap);
sem_init(&data_sem, 0, 0);
sem_init(&space_sem, 0, cap);
pthread_mutex_init(&c_mutex, nullptr);
pthread_mutex_init(&p_mutex, nullptr);
}
void Push(const T &in)
{
sem_wait(&space_sem);
pthread_mutex_lock(&p_mutex);
rq[p_pos++]=in;
p_pos%=_capacity;
pthread_mutex_unlock(&p_mutex);
sem_post(&data_sem);
}
void Pop(T &out)
{
sem_wait(&data_sem);
pthread_mutex_lock(&c_mutex);
out=rq[c_pos++];
c_pos%=_capacity;
pthread_mutex_unlock(&c_mutex);
sem_post(&space_sem);
}
~RingQueue()
{
sem_destroy(&data_sem);
sem_destroy(&space_sem);
pthread_mutex_destroy(&c_mutex);
pthread_mutex_destroy(&p_mutex);
}
private:
vector<T> rq;
int _capacity;
int c_pos=0;
int p_pos=0;
sem_t data_sem;
sem_t space_sem;
pthread_mutex_t c_mutex;
pthread_mutex_t p_mutex;
};
void* consumer(void* args)
{
RingQueue<int>* rq=(RingQueue<int>*)args;
while(true)
{
int out=0;
rq->Pop(out);
printf("consumer pop: %d\n", out);
}
}
void* productor(void* args)
{
RingQueue<int>* rq=(RingQueue<int>*)args;
while(true)
{
int in=rand()%100;
rq->Push(in);
printf("productor push: %d\n", in);
}
}
int main()
{
srand((unsigned int)time(nullptr));
RingQueue<int>* rq=new RingQueue<int>();
pthread_t c[2], p[2];
for (int i=0; i<2; i++)
pthread_create(c+i, nullptr, consumer, rq);
for (int i=0; i<2;i++)
pthread_create(p+i, nullptr, productor, rq);
for (int i=0; i<2; i++)
pthread_join(c[i], nullptr);
for (int i=0; i<2; i++)
pthread_join(p[i], nullptr);
delete rq;
return 0;
}
4.线程池
线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,从而避免了在处理短时间任务时创建与销毁线程的代价。
互斥锁的封装
pragma once
#include <iostream>
#include <pthread.h>
#include <mutex>
//对锁的封装
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p = nullptr): _lock_p(lock_p)
{}
void lock()
{
if(_lock_p) pthread_mutex_lock(_lock_p);
}
void unlock()
{
if(_lock_p) pthread_mutex_unlock(_lock_p);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock_p;
};
//通过构造函数和析构函数,实现对已有锁的加锁与解锁(通过对上面所封装锁类的接口的调用)
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex): _mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
线程的封装
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <pthread.h>
using namespace std;
typedef void *(*fun_t)(void *);
//线程函数执行时的参数集合
class ThreadData
{
public:
void *_args;//线程池的this指针
string _name;//线程的名称
};
class Thread
{
public:
//对线程名称、函数、传入参数进行初始化
Thread(int num, fun_t callback, void *args) : _func(callback)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
_name = nameBuffer;
_tdata._args = args;
_tdata._name = _name;
}
//创建线程
void start()
{
int n= pthread_create(&_tid, nullptr, _func, (void*)&_tdata);
assert(n == 0);
}
//线程等待
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
}
//得到线程名称
string threadname()
{
return _name;
}
private:
string _name;
fun_t _func;
ThreadData _tdata;
pthread_t _tid;
};
任务的封装
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <functional>
using namespace std;
int mymath(int x, int y, char op)
{
int result = 0;
switch (op)
{
case '+':
{
result = x + y;
}
break;
case '-':
{
result = x - y;
}
break;
case '*':
{
result = x * y;
}
break;
case '/':
{
if (y == 0)
{
std::cerr << "div zero error!" << std::endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
std::cerr << "mod zero error!" << std::endl;
result = -1;
}
else
result = x % y;
}
break;
}
return result;
}
class Task
{
typedef function<int(int, int, char)> func_t;
public:
Task(){}
Task(int x, int y, char op, func_t func)
:_x(x), _y(y), _op(op), _callback(func)
{}
//执行任务,并将结果返回
string operator()()
{
int result = _callback(_x, _y, _op);
char buffer[1024];
snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
return buffer;
}
//显示任务方法内容
string toTaskString()
{
char buffer[1024];
snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
return buffer;
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
线程池的封装
#include "Thread.hpp"
#include "LockGuard.hpp"
#include "Task.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
const int gnum=3;
template <class T>
class ThreadPool
{
private:
//该线程池中线程的执行函数,由于线程创建所需的函数的参数为 void* ,若是使用非静态成员函数,会存在 this指针,不能作为线程创建的参数进行传递
static void *routine(void *args)
{
ThreadData *td = (ThreadData *)args;
ThreadPool<T> *tp = (ThreadPool<T> *)td->_args;
while (true)
{
Task task;
//加锁代码块
{
LockGuard lockguard(tp->mutex());
while (tp->isQueueEmpty())
tp->threadWait();
task = tp->pop(); //将任务队列中的头部的任务提取出来后再对其进行处理(公共->私有)
}
cout<<td->_name<<" run: "<<task()<<endl;
}
}
public:
ThreadPool(const int &num=gnum) : _num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for (int i = 1; i <= _num; i++)
{
_threads.push_back(new Thread(i, routine, this));
}
}
void push(const T &in)
{
//生产者在push任务时,消费者(线程池中的线程)可能会读取任务,因此需要进行加锁
LockGuard lockguard(&_mutex);
_task_queue.push(in);
//在push后,队列必定不为空,激活环境变量
pthread_cond_signal(&_cond);
}
//通过将_task_queue的pop接口进行封装使得类内静态成员函数能够进行pop操作
T pop()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
void run()
{
for (const auto &t : _threads)
{
t->start();
cout << t->threadname() << " start ..." << endl;
}
}
void lockQueue() { pthread_mutex_lock(&_mutex); }
void unlockQueue() { pthread_mutex_unlock(&_mutex); }
bool isQueueEmpty() { return _task_queue.empty(); }
void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
//同pop操作,通过封装便于static函数读写成员变量
pthread_mutex_t *mutex() { return &_mutex; }
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for (const auto &t : _threads)
{
t->join();
delete t;
}
}
private:
int _num;
vector<Thread *> _threads;
queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
主函数
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
ThreadPool<Task>* tp=new ThreadPool<Task>();
tp->run();
while(true)
{
int x = rand()%100 + 1;
usleep(7721);
int y = rand()%30 + 1;
Task t(x, y, '+', mymath);
cout<<"make task done: "<<t.toTaskString()<<endl;
tp->push(t);
sleep(1);
}
return 0;
}
5.单例模式
某些类只应该具有一个对象,就称之为单例。
单例模式分为两种实现方式
饿汉方式:先创建好所需的对象,之后对其进行各式的操作。
懒汉方式:在第一次使用到该对象时将其创建。
例如上面的线程池,我们便可以通过对构造函数的隐藏(设为private)和封装一个创建函数来实现单例模式
private:
ThreadPool(const int &num=gnum) : _num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for (int i = 1; i <= _num; i++)
{
_threads.push_back(new Thread(i, routine, this));
}
}
public:
static ThreadPool<T> *getThreadPool(int num = gnum)
{
//外层的判断用于在加锁前检测是否为空,避免频繁加锁解锁影响效率
if (nullptr == thread_ptr)
{
//当多个线程同时调用该函数可能会创建多个对象,需要加锁
LockGuard lockguard(&s_mutex);
//在某一进程进入该if语句并在加锁前,其他进程可能已经创建好了对象并解锁,因此需要再做一次判断
if (nullptr == thread_ptr)
{
thread_ptr = new ThreadPool<T>(num);
}
}
return thread_ptr;
}
private:
int _num;
vector<Thread *> _threads;
queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T> *thread_ptr;
static pthread_mutex_t s_mutex;
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::s_mutex = PTHREAD_MUTEX_INITIALIZER;
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
ThreadPool<Task>::getThreadPool()->run();
while(true)
{
int x = rand()%100 + 1;
usleep(7721);
int y = rand()%30 + 1;
Task t(x, y, '+', mymath);
cout<<"make task done: "<<t.toTaskString()<<endl;
ThreadPool<Task>::getThreadPool()->push(t);
sleep(1);
}
return 0;
}
6.其他常见锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁,当其他线程想要访问数据时被阻塞挂起。(上面我们所使用到的都为悲观锁)
乐观锁:每次取数据时,总是乐观的认为数据不会被其他线程修改,因此不上锁,但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。
自旋锁:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,就一直循环等待判断该资源是否已经释放锁。而不是像互斥锁一样阻塞等待。而自旋锁与互斥锁的选择可以根据使用锁的时间来定。
7.读者写者问题
类似于生产者消费者模型,读者写者问题也有两种角色:读者和写者、三种关系。与生产者消费者模型本质的区别在于,该问题只会去读取已有的数据,而不会取走数据。因此,读者与读者之间不存在像消费者与消费者之间所存在的互斥。
#include<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//设置读写优先
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 写者优先,但写者不能递归加锁
*/