一.线程相关概念
1.线程的定义
进程中的一条执行流,是一个进程内的控制序列。
线程在linux下通过pcb实现,并且这些pcb共享了进程的大部分资源,相较于传统pcb更加轻量化,也称为轻量级进程
2.线程的独立和共享
独有:
标识符(唯一标识一个线程);
栈空间;
寄存器(存储上下文信息,状态信息,并不是物理上的硬件);
信号屏蔽字(信号的阻塞集合),线程信号的阻塞接口:pthread_sigmask;
errno(一个全局变量,保存上一次系统调用出错的原因编号),因为这是一个全局变量,所有线程都可以修改,多个线程对于错误原因使用同一个全局变量,那么错误原因就会不准确,所以为了让每个线程对自己的错误原因进行清晰的认识,每个线程有一个自己独立的errno;
优先级;
共享:
虚拟地址空间(代码段/数据段),
文件描述符表,
信号处理方式,
用户id/用户组id/工作路径
3.线程与进程之间的区别和联系
(1)联系
线程是进程中的一条执行流,同一进程的所属线程共享进程的资源。
(2)区别
进程是操作系统资源分配的最小单位,而线程是系统调度的最小单位;
进程有独立的空间地址,而线程没有独立的空间地址,多个线程共享所属进程的空间地址;
进程之间相互不影响,而同一进程的线程之间有同步和互斥关系,不同进程的线程间可以并发执行;
一个进程含有一至多个线程,而线程只能属于一个进程;
创建进程消耗时间较大,创建线程消耗时间较小;
4.多线程和多进程进行多任务处理的优缺点
多进程的优点:
稳定性更高,一个进程崩溃不会影响其他进程
多线程的优点:
(1)线程间通信更加灵活。除了进程间通信方式外,还可以通过全局变量和函数传参实现通信(线程间公用同一个进程的虚拟地址空间);
(2) 线程的创建和销毁成本更低。创建一个线程只需要创建一个pcb,而共有的数据通过一个指针指向同一个地址就可以实现共享;
(3)同一个进程中的线程切换调度成本更低。调度切换需要切换页表,多个线程间使用的是同一个页表,就不需要切换;
多线程的缺点:
稳定性差,一个线程崩溃,线程所在进程就会崩溃;
多进程的应用场景:
适用于对主进程安全性要求比较高的程序,比如shell,网络服务器
多线程的应用场景:
适用于多任务处理的场景
5.CPU密集型程序和IO密集型程序
(1)CPU密集型程序
当进行大量的数据运算时,如果CPU资源足够就可以同时处理,提高效率。通常执行流的个数是cpu核心数+1。
如果创建的线程很多,而cpu资源不够多,会造成线程切换调度成本的提高。
(2)IO密集型程序
主要有等待和数据拷贝两个过程,
多任务并行处理时,单磁盘可以并行压缩等待时间,多磁盘可以实现同时处理
6.线程使用起来有很多好处,是不是创建线程越多越好?
不是,因为CPU资源是固定的,如果线程多了,就会增加调度切换成本
二.线程控制
线程控制包括四个方面:
线程创建,线程终止,线程等待,线程分离
(1)线程创建
pthread_create函数
接口定义:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void), void *arg);
参数:
thread:返回线程ID,一个输入输出型参数,用来获取线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:
成功返回0;失败返回一个非零值,也就是错误码
线程id是什么?
每个线程在进程的PCB中会有自己的一块相对独立的地址空间,线程id就是这块空间的首地址
查看当前线程id的命令
ps -ef -L | head -n 1 && ps -ef -L | grep 程序名
(2)线程退出
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 在线程入口函数return退出。这种方法对主线程不适用,从main函数return相当于退出进程
- 线程可以调用pthread_ exit终止自身;
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程;
疑难点
进程退出,会退出所有的线程.
主线程中退出,并不会导致进程退出,除非使用return;
接口介绍:
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:线程退出的返回值
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
(3)线程等待
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数
功能:一个阻塞接口,等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
(4)线程分离
默认情况下,新创建的线程是处于joinable状态的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
pthread_detach函数
功能:分离一个线程
原型:
int pthread_detach(pthread_t thread)
参数:线程id
返回值:成功返回0;失败返回错误码
三.线程安全
1.基本概念
1).什么是线程安全?
多个线程访问临界资源时,因为线程间的操作相互独立,对临界资源的访问是不安全的,这就是线程安全问题
线程安全需要通过同步和互斥实现
2).重入
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入
3).可重入函数
一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
4).可重入和线程安全的区别和联系
区别
可重入函数是线程安全函数的一种;
线程安全不一定是可重入的,而可重入函数则一定是线程安全的;
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的;
联系
函数是可重入的,那就是线程安全的;
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的;
2.线程互斥
1)互斥锁
本质上是一个只有0和1两种状态的计数器,用于标记临界资源的访问状态,0表示不能访问,1表示可以访问。
在进入临界区之前加锁,判断资源是否可访问,
在退出临界区之后解锁,将资源状态置为可访问状态
互斥锁自身的计数操作是一个原子操作,它是线程安全的
2)互斥锁常见操作接口
初始化pthread_mutex_init
函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:互斥量
attr:互斥量的属性,通常设置为NULL
返回值:成功返回0,失败返回错误编号
销毁pthread_mutex_destroy
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号
加锁pthread_mutex_lock
函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
作用:阻塞加锁
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号
下面的接口也可以加锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
作用:非阻塞加锁
这个接口返回值成功返回0,失败返回错误编号
解锁pthread_mutex_unlock
函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:mutex:互斥量
返回值:成功返回0,失败返回错误编号
3)互斥锁的使用流程
- 定义互斥锁变量
pthread_mutex_t mtx;
-
初始化互斥锁变量,有两种方式
第一种:在定义时使用宏PTHREAD_MUTEX_INITIALIZER初始化;
第二种:使用pthread_mutex_init接口 -
在临界资源访问之前加锁
-
在临界资源访问之后解锁
-
释放互斥锁
4)互斥锁相关知识
互斥锁是一种不可重入锁,也就是说只能加锁一次,加锁之后必须解锁才能继续使用。
5)死锁
程序运行因为某种原因卡死,无法继续运行,常见的比如资源不足;
死锁产生的四个必要条件
互斥条件;
资源在同一时刻只能有一个进程或线程访问
不可剥夺条件;
对于本进程或线程拥有的资源,只能自己主动释放,不允许别的进程或线程抢占当前拥有的资源;
请求和保持条件;
请求资源请求不到,不会释放自身已有的资源
环路等待条件
当前进程组/线程组中的每个进程/线程都在等到该组的其它进程/线程释放资源
死锁如何处理?
预防死锁:
破坏死锁的四个必要条件中的请求和保持条件或者环路等待条件,而互斥条件和不可剥夺条件是无法人为控制的。
在程序中可以提前计算好资源的请求和释放顺序;
请求不到新资源就释放已有资源;
避免死锁:
银行家算法和死锁检测算法
3.线程同步
通过条件判断实现对资源获取的合理性
在Linux下可以通过条件变量和信号量两种方式来实现
条件变量实现同步
条件变量的概念
条件变量是一种“事件通知机制”,它本身不提供、也不能够实现“互斥”的功能。因此,条件变量通常(也必须)配合互斥量来一起使用,其中互斥量实现对“共享数据”的互斥(即同步),而条件变量则去执行 “通知共享数据状态信息的变化”的任务。
条件变量本身并不确定资源什么时候获取合理,资源是否获取合理需要自行判断
条件变量函数
初始化
有两种初始化方式:
第一种:
通过pthread_cond_init接口
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
返回值:成功返回0,失败返回错误编号
第二种:通过PTHREAD_COND_INITIALIZER这个宏在定义时初始化
pthread_cond_t con = PTHREAD_COND_INITIALIZER;
销毁pthread_cond_destroy
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond)
参数: cond:要初始化的条件变量
返回值:成功返回0,失败返回错误编号
阻塞等待条件满足pthread_cond_wait
函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:等待的条件变量
mutex:互斥量
返回值:成功返回0,失败返回错误编号
为什么pthread_ cond_ wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
唤醒等待pthread_cond_signal
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
作用:至少唤醒一个线程
参数:cond:要唤醒的条件变量
返回值:成功返回0,失败返回错误编号
唤醒所有PCB等待队列的线程pthread_cond_broadcast
int pthread_cond_broadcast(pthread_cond_t *cond);
条件变量使用中的注意事项
- 条件变量使用过程中,条件的判断应该采用循环操作
- 使用过程中,如果有多种角色就需要使用多个条件变量,不同角色的分开等待和唤醒;
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//模拟顾客在餐馆就餐问题
#define MAX_COOKER 4 //厨师数
#define MAX_CUSTOMER 6 //顾客数
int bowl = 0; //规定每次只能有一个人在吃饭,因为这是一个VIP餐厅,提供一对一服务,bowl为0表示没有饭,bowl为1表示有饭
pthread_mutex_t mtx; //互斥锁
pthread_cond_t cond_cooker; //厨师相关条件变量
pthread_cond_t cond_customer; //顾客相关条件变量
//厨师线程入口函数
void* cookerEntry(void* arg)
{
//厨师一直在不停的做饭
while(1)
{
//1.加锁
pthread_mutex_lock(&mtx);
//2.判断是否有饭
//有饭就阻塞等待
//没有饭就做饭
//做好饭之后唤醒等待的顾客
//为什么是循环进行判断,这是因为当线程被唤醒时,有可能其他线程也被唤醒了,这时候资源被使用了,继续
//使用资源是不合理的
//在这个应用场景中,也就是现在没饭了,唤醒了厨师,但是其他厨师已经快人一步,将饭做好了,
//这时候继续做饭就是不合理的,应该继续判断当前的状态
while(bowl == 1)
{
//有饭就阻塞自己,阻塞操作需要循环进行
pthread_cond_wait(&cond_cooker, &mtx);
}
//没有饭开始做饭
printf("厨师正在做饭~\n");
//厨师需要时间去做饭
sleep(1);
++bowl;
//做好饭之后,唤醒顾客去吃饭
pthread_cond_signal(&cond_customer);
//3.解锁
pthread_mutex_unlock(&mtx);
}
return NULL;
}
//顾客线程入口函数
void* customerEntry(void* arg)
{
while(1)
{
//1.加锁
pthread_mutex_lock(&mtx);
//2.判断是否有饭
//有饭就吃饭
//吃完饭后唤醒等待的厨师
//没有饭就阻塞等待
//为什么是循环进行判断,这是因为当线程被唤醒时,有可能其他线程也被唤醒了,这时候资源被使用了,继续
//使用资源是不合理的
//在这个例子中,也就是现在有饭了,顾客被唤醒,但是其他顾客先人一步,将饭吃了,那么这时候继续吃饭就没饭了
//需要继续判断当前到底有没有饭
while(bowl == 0)
{
//没有饭阻塞等待
pthread_cond_wait(&cond_customer, &mtx);
}
//有饭
printf("好吃~\n");
//顾客需要时间去吃饭
sleep(1);
--bowl;
//唤醒厨师
pthread_cond_signal(&cond_cooker);
//3.解锁
pthread_mutex_unlock(&mtx);
}
return NULL;
}
int main()
{
//初始化互斥锁和条件变量
pthread_mutex_init(&mtx, NULL);
pthread_cond_init(&cond_cooker, NULL);
pthread_cond_init(&cond_customer, NULL);
pthread_t cookers[MAX_COOKER]; //厨师数组
pthread_t customers[MAX_CUSTOMER]; //顾客数组
//创建厨师线程
for(int i = 0; i < MAX_COOKER; ++i)
{
int ret = pthread_create(&cookers[i], NULL, cookerEntry, NULL);
if(ret != 0)
{
printf("create cooker thread failed!\n");
continue;
}
}
//创建顾客线程
for(int i = 0; i < MAX_CUSTOMER; ++i)
{
int ret = pthread_create(&customers[i], NULL, customerEntry, NULL);
if(ret != 0)
{
printf("create customer thread failed!\n");
continue;
}
}
//主线程阻塞等待线程退出
for(int i = 0; i < MAX_COOKER; ++i)
{
pthread_join(cookers[i], NULL);
}
for(int i = 0; i < MAX_CUSTOMER; ++i)
{
pthread_join(customers[i], NULL);
}
//销毁互斥锁和条件变量
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond_cooker);
pthread_cond_destroy(&cond_customer);
return 0;
}
4.posix信号量实现同步和互斥
1)信号量回顾
信号量是程序中的一个计数器,加上一个PCB等待队列,用于实现进程和线程间的同步与互斥
2) 使用流程及接口
- 定义信号量
sem_t sem; - 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value); - 进行P操作
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); - 进行V操作
int sem_post(sem_t *sem); - 销毁信号量
int sem_destroy(sem_t *sem);
3) 信号量实现生产者消费者模型
注意事项:
加锁和判断的顺序应该是先判断后加锁,有以下两点原因:
- 如果先加锁,判断时发生阻塞,那么锁就无法释放;
- 加锁会降低效率,加锁永远只保护需要保护的操作
RingQueue.h:
#pragma once
#include <vector>
#include <semaphore.h>
#define MAX_CAPACITY 6 //队列的容量
//使用信号量实现一个线程安全的阻塞队列
//信号量不仅仅是一个计数器,还有一个等待队列
//当数据不满足条件时,将请求进程放到等待队列中
class RingQueue
{
private:
int _capacity; //当前队列所能存储的容量,有一个空间不能使用,用来标记队列满的情况
int _read; //读指针,指向当前可以读取的位置
int _write; //写指针,指向当前可以写的位置
std::vector<int> _data; //数据域,存储数据
sem_t _sem_mutex; //用于实现互斥的信号量
sem_t _sem_idle; //用于实现生产者同步的信号量
sem_t _sem_data; //用于实现消费者同步的信号量
public:
RingQueue(int cap = MAX_CAPACITY)
:_capacity(cap + 1)
,_read(0)
,_write(0)
{
//多开辟一个大小的空间,是为了区分当前队列是否已满,队列中满的时候的容量是_capacity,有一个空间不放置数据
_data.resize(cap + 1);
//实现互斥的信号量初始化为1
//int sem_init(sem_t *sem, int pshared, unsigned int value);
//pshared为0表示初始化用于线程间的信号量,非0表示用于进程间
sem_init(&_sem_mutex, 0, 1);
//实现生产者同步的信号量初始化为cap,表示现在可以写入cap个数据
sem_init(&_sem_idle, 0, cap);
//实现消费者同步的信号量初始化为0,表示没有可以使用的数据
sem_init(&_sem_data, 0, 0);
}
~RingQueue()
{
sem_destroy(&_sem_mutex);
sem_destroy(&_sem_idle);
sem_destroy(&_sem_data);
}
//关于Push和Pop为什么是先判断后加锁呢?
//1.如果先加锁,判断时发生阻塞,那么锁就无法释放;
//2.加锁会降低效率,加锁永远只保护需要保护的操作
bool Push(int val)
{
//判断能否入队数据
//空闲节点数-1,如果小于0阻塞
//这个操作内部本身是有实现线程安全的
sem_wait(&_sem_idle);
//互斥访问保护数据操作
//加锁
sem_wait(&_sem_mutex);
//插入数据
_data[_write] = val;
//更新写指针
_write = (_write + 1) % _capacity;
//解锁
sem_post(&_sem_mutex);
//数据节点数+1,唤醒消费者
sem_post(&_sem_data);
return true;
}
//出队的数据存放到data这个输入输出型参数中
bool Pop(int* data)
{
//判断能否出队数据
//数据节点-1,小于0进行阻塞,这个操作内部是线程安全的
sem_wait(&_sem_data);
//互斥信号量保护数据读取操作
//加锁
sem_wait(&_sem_mutex);
//将要读取的数据写入data这个输入输出型参数中
*data = _data[_read];
//更新读指针
_read = (_read + 1) % _capacity;
//唤醒生产者
//解锁
sem_post(&_sem_mutex);
//空闲节点+1,唤醒生产者线程
sem_post(&_sem_idle);
return true;
}
//判断当前队列是否为空
bool empty()
{
//当读指针和写指针在同一个位置,说明还没有写入数据
return _read == _write;
}
//判断当前队列是否已满
bool full()
{
//当写指针的下一个位置是读指针,说明已经写入了_capacity个数据,数据域满了
return _read == (_write + 1) % _capacity;
}
};
pro_cus.cpp:
#include <iostream>
#include "RingQueue.h"
#include <unistd.h>
#define MAX_PRODUCER 5 //生产者的最大数目
#define MAX_CUSTOMER 6 //消费者的最大数目
using std::cout;
using std::endl;
//生产者线程
void* producerEntry(void* arg)
{
RingQueue* q = reinterpret_cast<RingQueue*>(arg);
while(1)
{
static int i = 1;
bool ret = q->Push(i++);
if(!ret)
{
cout << "thread " << pthread_self() << " Push data " << i << " is failed" << endl;
}
else
{
cout << "thread " << pthread_self() << "Push data " << i << " is successful" << endl;
}
}
return NULL;
}
//消费者线程
void* customerEntry(void* arg)
{
RingQueue* q = reinterpret_cast<RingQueue*>(arg);
int data;
while(1)
{
bool ret = q->Pop(&data);
if(!ret)
{
cout << "thread " << pthread_self() << " Pop data failed" << endl;
}
else
{
cout << "thread " << pthread_self() << " Pop data successfully , data is " << data << endl;
}
}
return NULL;
}
int main()
{
//生产者和消费者县城要使用同一个环形队列存储数据,这样数据才能进行通信
RingQueue q;
pthread_t pro_tid[MAX_PRODUCER];
pthread_t cus_tid[MAX_CUSTOMER];
int ret;
//创建生产者线程
for(int i = 0; i < MAX_PRODUCER; ++i)
{
ret = pthread_create(&pro_tid[i], NULL, producerEntry, &q);
if(ret != 0)
{
cout << "创建生产者线程失败" << endl;
continue;
}
else
{
cout << "创建生产者线程成功,线程id是" << pro_tid[i] << endl;
}
}
//创建消费者线程
for(int i = 0; i < MAX_CUSTOMER; ++i)
{
ret = pthread_create(&cus_tid[i], NULL, customerEntry, &q);
if(ret != 0)
{
cout << "创建消费者线程失败" << endl;
continue;
}
else
{
cout << "创建消费者线程成功,线程id是" << cus_tid[i] << endl;
}
}
//线程等待,让主进程不退出
for(int i = 0; i < MAX_PRODUCER; ++i)
{
ret = pthread_join(pro_tid[i], NULL);
if(ret != 0)
{
cout << "阻塞等待线程 " << pro_tid[i] << " 失败" << endl;
continue;
}
else
{
cout << "阻塞等待线程 " << pro_tid[i] << " 成功" << endl;
}
}
for(int i = 0; i < MAX_CUSTOMER; ++i)
{
ret = pthread_join(cus_tid[i], NULL);
if(ret != 0)
{
cout << "阻塞等待线程 " << cus_tid[i] << " 失败" << endl;
continue;
}
else
{
cout << "阻塞等待线程 " << cus_tid[i] << " 成功" << endl;
}
}
return 0;
}
5.信号量和条件变量的区别
- 信号量本质是一个程序中的计数器+一个PCB等待队列,而条件变量的本质是一个PCB等待队列;
- 条件变量需要搭配互斥锁一起使用,信号量不需要,信号量内部是线程安全的;
- 条件变量的资源获取条件需要自己判断,信号量通过自身计数完成;
四.线程应用
1.生产者与消费者模型
一种进程负责产生数据,另一方负责处理数据,双方将数据放到公共的缓冲区中,这个公共缓冲区的数据放入和拿出必须是线程安全的
优点:解耦合,支持忙闲不均,支持并发
代码实现:
BlockQueue.h:
#pragma once
#include <queue>
#include <pthread.h>
//线程安全的阻塞队列
#define MAX_CAPACITY 6 //阻塞队列中的最大容量
class BlockQueue
{
private:
size_t _capacity = MAX_CAPACITY; //队列的最大容量
pthread_mutex_t _mtx; //互斥锁
pthread_cond_t _cond_producer; //生产者相关的条件变量
pthread_cond_t _cond_consumer; //消费者相关的条件变量
std::queue<int> _queue;
public:
BlockQueue()
{
//在构造函数中初始化互斥锁和条件变量
pthread_mutex_init(&_mtx, NULL);
pthread_cond_init(&_cond_producer, NULL);
pthread_cond_init(&_cond_consumer, NULL);
}
~BlockQueue()
{
//在析构函数中销毁互斥锁和环境变量
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond_producer);
pthread_cond_destroy(&_cond_consumer);
}
//生产者向队列中写数据
bool Push(int data)
{
//1.加锁
pthread_mutex_lock(&_mtx);
//2.判断队列中是否已经满了
while(_queue.size() == _capacity)
{
//队列中数据满了,就阻塞等待,直到队列中可以写数据
pthread_cond_wait(&_cond_producer, &_mtx);
}
//队列中可以写数据,将数据写入
_queue.push(data);
//唤醒等待的消费者线程
pthread_cond_signal(&_cond_consumer);
//3.解锁
pthread_mutex_unlock(&_mtx);
return true;
}
//消费者队列出对数据
bool Pop(int* data)
{
//1.加锁
pthread_mutex_lock(&_mtx);
//2.判断队列中是否为空
while(_queue.empty())
{
//队列为空,消费者阻塞等待
pthread_cond_wait(&_cond_consumer, &_mtx);
}
//队列中有数据就出队
*data = _queue.front();
_queue.pop();
//唤醒等待的生产者进程
pthread_cond_signal(&_cond_producer);
//3.解锁
pthread_mutex_unlock(&_mtx);
return true;
}
};
pro_cus.cpp:
#include <iostream>
#include <unistd.h>
#include "BlockQueue.h"
using std::cout;
using std::endl;
#define MAX_PRODUCER 3
#define MAX_CONSUMER 5
//生产者线程入口函数
void* producerEntry(void* arg)
{
//之前在这里类型强转时使用普通变量来接收,这样是有问题的
//BlockQueue q = *(BlockQueue*)arg;
BlockQueue* q = (BlockQueue*)arg;
static int i = 1;
while(1)
{
q->Push(i);
cout << pthread_self() << " write data : " << i << endl;
++i;
}
return NULL;
}
//消费者线程入口函数
void* consumerEntry(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int buf;
while(1)
{
q->Pop(&buf);
cout << pthread_self() << " read from queue is " << buf << endl;
}
return NULL;
}
int main()
{
pthread_t pro_tid[MAX_PRODUCER];
pthread_t con_tid[MAX_CONSUMER];
//创建同一个阻塞队列作为缓冲区
BlockQueue q;
//创建生产者线程
for(int i = 0; i < MAX_PRODUCER; ++i)
{
int ret = pthread_create(&pro_tid[i], NULL, producerEntry, (void*)&q);
if(ret == 0)
{
cout << "create thread successfully!" << endl;
continue;
}
}
//创建消费者线程
for(int i = 0; i < MAX_CONSUMER; ++i)
{
int ret = pthread_create(&con_tid[i], NULL, consumerEntry, (void*)&q);
if(ret == 0)
{
cout << "create thread successfully!" << endl;
continue;
}
}
//等待线程退出
for(int i = 0; i < MAX_PRODUCER; ++i)
{
pthread_join(pro_tid[i], NULL);
}
for(int i = 0; i < MAX_CONSUMER; ++i)
{
pthread_join(con_tid[i], NULL);
}
return 0;
}
2.线程池
1) 线程池的工作原理
创建好一堆线程(有最大数量上限)和一个任务队列,当有新任务到来时,将新任务放到任务队列中,线程池中的线程从任务队列中获取任务进行处理
2)使用场景
针对大量请求进行处理
3)优势
- 避免了因为大量创建和销毁线程而带来的时间成本;
- 线程池中的线程数量和任务队列中存储的任务有最大上限,避免了峰值压力下资源耗尽,系统崩溃的风险;
4)实现一个线程池
- 定义一个任务节点类,通过实例化一个任务节点对象,调用对象的方法进行任务处理
- 实现一个线程安全的任务队列;
- 定义一个线程池类,
这个类有三个成员:最大线程数、最大任务节点数、线程安全队列;
提供可以指向线程数和任务节点数目的构造函数,提供创建线程的初始化接口,提供任务节点入队接口;
代码实现:
ThreadPool.h:
#pragma once
#include <queue>
#include <pthread.h>
#include <cstdio>
#include <errno.h>
#include <unistd.h>
typedef void(*handler_t)(int); //定义一个函数指针类型
//任务节点
class TaskNode
{
private:
handler_t _handler; //任务节点对应的处理函数
int _data; //任务节点处理的数据
public:
//空的构造函数保证能实例化一个无参对象
TaskNode(){}
//任务节点在构造时将处理的任务函数和参数传入
TaskNode(handler_t handler, int data)
:_handler(handler)
,_data(data)
{}
//设置任务和参数
void SetTask(handler_t handler, int data)
{
_handler = handler;
_data = data;
}
void Run()
{
_handler(_data);
}
};
//***********************线程安全的阻塞队列******************************
#define MAX_CAPACITY 6 //阻塞队列中的最大容量
class BlockQueue
{
private:
size_t _capacity; //队列的最大容量
pthread_mutex_t _mtx; //互斥锁
pthread_cond_t _cond_producer; //生产者相关的条件变量
pthread_cond_t _cond_consumer; //消费者相关的条件变量
std::queue<TaskNode> _queue;
public:
BlockQueue(int cap = MAX_CAPACITY)
:_capacity(cap)
{
//在构造函数中初始化互斥锁和条件变量
pthread_mutex_init(&_mtx, NULL);
pthread_cond_init(&_cond_producer, NULL);
pthread_cond_init(&_cond_consumer, NULL);
}
~BlockQueue()
{
//在析构函数中销毁互斥锁和环境变量
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond_producer);
pthread_cond_destroy(&_cond_consumer);
}
//生产者向队列中写数据
bool Push(const TaskNode& data)
{
//1.加锁
pthread_mutex_lock(&_mtx);
//2.判断队列中是否已经满了
while(_queue.size() == _capacity)
{
//队列中数据满了,就阻塞等待,直到队列中可以写数据
pthread_cond_wait(&_cond_producer, &_mtx);
}
//队列中可以写数据,将数据写入
_queue.push(data);
//唤醒等待的消费者线程
pthread_cond_signal(&_cond_consumer);
//3.解锁
pthread_mutex_unlock(&_mtx);
return true;
}
//消费者队列出队数据
bool Pop(TaskNode* data)
{
//1.加锁
pthread_mutex_lock(&_mtx);
/*
进行非阻塞等待也是可以的
while(pthread_mutex_trylock(&_mtx) == EBUSY)
{
printf("Inside BlockQueue Pop, trylock failed!\n");
usleep(500000);
}
*/
//2.判断队列中是否为空
while(_queue.empty())
{
//队列为空,消费者阻塞等待
pthread_cond_wait(&_cond_consumer, &_mtx);
}
//队列中有数据就出队
*data = _queue.front();
_queue.pop();
//唤醒等待的生产者进程
pthread_cond_signal(&_cond_producer);
//3.解锁
pthread_mutex_unlock(&_mtx);
return true;
}
};
#define MAX_THREAD_NUM 6 //线程池中能创建的最大线程数
#define MAX_QUEUE_NODE_NUM 10 //线程池中的任务队列最多存储的节点数
//*******************************线程池类*************************************
class ThreadPool
{
private:
int _max_thread_num; //最大线程数
int _max_queue_node_num; //任务队列中最大节点数
BlockQueue _queue; //线程安全的任务队列
private:
//线程入口函数,负责将任务节点的处理函数转换为创建线程要求的格式,并执行任务处理动作
//设置为静态的成员函数,避免this指针造成的影响
static void* Entry(void* arg)
{
//这里在类型转换的时候太粗心,将arg转换为了BlockQueue*!!!!!!!!!!!!!!!
ThreadPool* tp = (ThreadPool*)arg;
while(1)
{
TaskNode task;
//拿到任务处理队列中的任务,并让其出队
tp->_queue.Pop(&task);
task.Run();
}
return NULL;
}
public:
//构造函数中不能进行初始化,防止异常产生
ThreadPool(int threadNum = MAX_THREAD_NUM, int queueNodeNum = MAX_QUEUE_NODE_NUM)
:_max_thread_num(threadNum)
,_max_queue_node_num(queueNodeNum)
,_queue(BlockQueue(queueNodeNum))
{}
//创建线程
bool Init()
{
pthread_t tid;
int ret;
for(int i = 0; i < _max_thread_num; ++i)
{
ret = pthread_create(&tid, NULL, Entry, (void*)this);
if(ret != 0)
{
printf("创建线程失败!\n");
return false;
}
//线程分离,让线程退出时自动释放资源
pthread_detach(tid);
}
return true;
}
//任务节点入队
bool Push(const TaskNode& task)
{
_queue.Push(task);
return true;
}
};
test.cpp:
#include <cstdio>
#include <unistd.h>
#include "ThreadPool.h"
void taskFun(int data)
{
printf("我是线程 %p , 我拿到了数据 %d, 我要休眠1s\n", pthread_self(), data);
sleep(1);
}
void test_threadpool()
{
ThreadPool threadPool;
TaskNode task;
threadPool.Init();
for(int i = 0; i < 10; ++i)
{
task.SetTask(taskFun, i);
bool ret = threadPool.Push(task);
if(ret)
{
printf("push task successful!\n");
}
}
while(1)
{
printf("我是主线程!\n");
sleep(1);
}
}
int main()
{
test_threadpool();
return 0;
}
3.线程安全的单例模式
1)什么是单例模式?
一种创建类型的设计模式,规定一个类只能实例化一个对象
2 )两种类型的单例模式
饿汉模式
提前创建好一份对象实例,资源在启动时就加载
代码实现:
#pragma once
template<class T>
class SingleTon
{
public:
static SingleTon<T>* getInstance()
{
return _instance;
}
private:
static SingleTon<T>* _instance; //这个类实例化后的唯一的一份资源
private:
SingleTon(){}
SingleTon(const SingleTon<T>& obj) = delete;
SingleTon<T>& operator=(const SingleTon<T>& obj) = delete;
};
//静态成员类外实例化
template<class T>
SingleTon<T>* SingleTon<T>::_instance = new SingleTon<T>();
懒汉模式
资源并不会一开始就加载,而是在第一次使用的时候加载,是一种延时加载的思想
代码实现:
#pragma once
#include <pthread.h>
template<class T>
class SingleTon
{
private:
static SingleTon<T>* _instance; //对应的实例化对象
static pthread_mutex_t* _mtx; //用于保证线程安全的互斥锁
private:
SingleTon()
{
//构造函数中初始化互斥锁
pthread_mutex_init(_mtx, NULL);
}
//删除拷贝构造和赋值运算符重载
SingleTon(const SingleTon<T>& obj) = delete;
SingleTon<T>& operator=(const SingleTon<T>& obj) = delete;
public:
//提供销毁单例对象的接口
void Destroy()
{
delete _instance;
pthread_mutex_destroy(_mtx);
}
static SingleTon<T>* getInstance()
{
//第一层判断,提高效率
if(_instance == nullptr)
{
pthread_mutex_lock(_mtx);
//第二层判断,在资源不存在时实例化资源
if(_instance == nullptr)
{
_instance = new SingleTon<T>;
}
pthread_mutex_unlock(_mtx);
}
return _instance;
}
};
template<class T>
SingleTon<T>* SingleTon<T>::_instance = nullptr; //将资源初始化为空
template<class T>
pthread_mutex_t* SingleTon<T>::_mtx = new pthread_mutex_t();
对于懒汉模式的实现,因为对泛型不熟悉,个人觉得有一些小问题,但是暂时找不出来,希望各位指点!!!!!!!!