=====================================================================================
功能:进行多任务处理
一个进程能够完成任务的处理,通过pcb的描述,完成对程序的调度处理。
在进程中,我们说进程就是一个pcb,是程序动态运行的描述,通过pcb可以实现操作系统对程序运行的调度管理。现在学习的多线程,我们说线程是进程中的一条执行流,这个执行流在Linux下是通过pcb实现的,因此实际上Linux下的线程就是一个pcb,然而pcb是进程,并且Linux下的pcb共用一个虚拟地址空间,相较于传统pcb更加轻量化,因此也被称为轻量级进程。
== Linux下的进程其实是一个线程组,一个进程中可以有多个线程(多个pcb),编程是进程中的一条执行流。(进程(一个工厂),线程(工厂中干活的工人),在Linux下干活的就是pcb)。==
进程:是一个程序动态的运行,其实就是一个程序运行的描述 - - - pcb。
线程:是进程中的一条执行流,执行一个程序中的某段代码。
在Linux下pcb可以实现程序的调度运行,因此在实现线程的时候,使用了pcb来实现;创建线程会伴随在内核中创建一个pcb来实现程序的调度,作为进程中的一条执行流。进程就是多个线程的一个合集,并且这个进程中的所有pcb共用进程中的大部分资源(程序运行时,操作系统为程序运行所分配的所有资源),因此这些pcb在Linux下又称为轻量级进程。
根据不同学习阶段,对pcb有不同的理解
进程是操作系统资源分配的基本单位;(操作系统会为一个程序的的运行分配所需的所有资源)
线程是cpu调度的基本单位
适用于对于主程序安全性要求很高的场景:shell/网络服务器
通过代码实现线程的创建 / 退出 / 等待 / 分离
进行线程控制的接口代码,其实都是库函数,也就是说,操作系统其实并没有提供创建线程的接口,因此人们在用户态使用函数封装了一套线程库,这套封装的线程库函数,提供了线程的各种操作。
使用库函数创建一个线程(用户态线程),本质上是在内核中创建一个轻量级进程来实现程序的调度。
thread:输出型参数,用于获取线程id(线程的操作句柄);
attr:线程属性,用于在创建线程的同时设置线程属性,通常置NULL;
start_routine:函数指针,这是一个线程的入口函数(线程运行的就是这个函数,函数运行完毕,则线程就会退出);
arg:通过线程入口函数,传递给线程的参数;
返回值:成功返回0,失败返回非0值(错误编号);
tid是一个无符号长整型数据。一个线程有一个pcb,每个pcb都有一个pid(是一个整型数据)。
tid和pid有什么联系?
tid是一个用户态线程的id,线程的操作句柄,这个id其实就是线程独有的这块空间的首地址。
每个线程被创建出来后,都会开辟一块空间,存储自己的栈,自己的描述信息
pid是一个轻量级进程id,是内核中task_struct结构体中的id;
task_struct ->pid:轻量级进程id,也就是ps -efL看到的LWP;
task_struct ->tgid:线程组id,等于主线程id (也就是外边所看到的进程id);
线程的操作大都是通过tid完成的
#include
#include //sleep头文件
#include //字符串操作头文件
#include //线程库接口头文件
int a = 100;
void *thr_start(void *arg)
{
while(1) {
printf("i am thread~~%s ---- %d\n", (char*)arg, a);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
char ptr[1024] = "chilema~~?";
//pthread_create(获取线程id, 线程属性, 线程入口函数, 参数)
int ret = pthread_create(&tid, NULL, thr_start, (void*)ptr);
if (ret != 0) {
printf("create thread failed!!\n");
return -1;
}
printf("create thread success!! normal thread tid:%lu\n", tid);
while(1) {
printf("leihoua~~ ----%d\n", a);
sleep(1);
}
return 0;
}
如何退出一个线程
等待一个线程的退出,获取退出线程的返回值,回收线程所占的资源。
默认情况下,一个线程必须被等待,若不等待,则会造成资源泄露。
将线程 joinable 属性修改成 detach 属性
分离一个线程,一定是对线程的返回值不感兴趣,根本就不想获取,但又不想一直等待线程退出,这种情况才会分离线程。
#include
#include
#include
#include
void function()
{
//char ptr[] = "这是我的返回值";//ptr有一块空间在栈中,将字符串的值赋值进去
char *ptr = "这是我的返回值"; //字符串在常量区,ptr只是保存了这个常量区的地址
pthread_exit((void*)ptr);//在任意位置调用,都可以退出调用线程
}
void *thr_start(void *arg)
{
//pthread_self 返回调用线程的tid
//pthread_detach(pthread_t tid);
pthread_t tid = pthread_self();
pthread_detach(tid);//自己分离自己---实际上就是设置个属性而已
while(1) {
printf("i am normal thread\n");
sleep(5);
function();
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if (ret != 0) {
printf("create thread failed!!\n");
return -1;
}
//char *ptr;
//pthread_join(tid, (void**)&ptr);
//printf("retval:%s\n", ptr);
//pthread_cancel(tid);
while(1) {
printf("i am main thread\n");
sleep(1);
}
return 0;
}
多个执行流对临界资源争抢访问,但是不会出现数据二义性。
同步:通过条件判断保证对临界资源访问的合理性;
互斥:通过同一时间对临界资源访问的唯一性实现临界资源访问的安全性;
互斥锁实现互斥原理:互斥锁本身是一个只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源时都需要先判断当前的临界资源状态是否允许访问,如果不允许则让执行流等待,否则可以让执行流访问临界资源,但是在访问期间需要将状态修改为不可访问状态,这期间不允许其他执行流进行访问。
所有的执行流都需要通过同一个互斥锁实现互斥,意味着互斥锁本身就是一个临界资源,大家都会访问。
#include
#include
#include
int ticket = 100;
pthread_mutex_t mutex;
void *thr_scalpers(void *arg)
{
while(1) {
//加锁一定是只保护临界资源的访问
pthread_mutex_lock(&mutex);
if (ticket > 0) {
//有票就一直抢
usleep(1000);
printf("%p-I got a ticket:%d\n", pthread_self(), ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}else {
//加锁后在任意有可能退出线程的地方都要解锁
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i, ret;
//互斥锁的初始化一定要放在线程创建之前
pthread_mutex_init(&mutex, NULL);
for (i = 0; i< 4; i++) {
ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);
if (ret != 0) {
printf("thread create error");
return -1;
}
}
for (i = 0; i < 4; i++) {
pthread_join(tid[i], NULL);
}
//互斥锁的销毁一定是不再使用这个互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
互斥锁本身的操作首先必须是安全的,互斥锁自身计数的操作是原子操作。
不管当前mutex的状态是什么,反正一步交换后,其他的线程都是不可访问的;这时候当前进程就可以慢慢判断了;
多个执行流对锁资源进行争抢访问,但是因为访问推进顺序不当,造成互相等待最终导致程序流程无法继续推进,这时候就造成了死锁,(死锁实际上就是一种程序流程无法继续推进,卡在某个位置)。
死锁产生的必要条件(有一条不具备就不会产生死锁)
死锁的预防:破坏死锁产生的必要条件(主要避免3和4两个条件的产生)。
死锁的避免:死锁检测算法 / 银行家算法
银行家算法的思路:系统的安全状态/非安全状态
一张表记录当前有哪些锁,一张表记录已经给谁分配了哪些锁,一张表记录谁当前需要哪些锁
按照三张表进行判断,判断若给一个执行流分配了指定的锁,是否会达成环路等待条件,导致系统的运行进入不安全状态,如果有可能就不能分配。反之,若分配了之后不会造成环路等待,系统是安全的,则分配这个锁,(破坏环路等待条件)。
后续若不能分配锁,可以资源回溯,把当前执行流中已经加了的锁释放掉,(破坏请求与保持)。
死锁是如何产生的?如何预防和避免?
加锁对临界资源进行保护,实际上对程序的性能时一个极大的挑战。在高性能程序中会将就一种无锁编程(CAS锁 / 一对一的阻塞队列 / atomic原子操作)。
同步的实现:通过条件判断实现临界资源访问的合理性(条件变量);
使用接口介绍
#include
#include
#include
#include
int bowl = 0;//默认0表示碗中没有饭
pthread_cond_t cook_cond; // 实现线程间对bowl变量访问的同步操作
pthread_cond_t customer_cond; // 实现线程间对bowl变量访问的同步操作
pthread_mutex_t mutex; // 保护bowl变量的访问操作
void *thr_cook(void *arg)
{
while(1) {
//加锁
pthread_mutex_lock(&mutex);
while (bowl != 0){//表示有饭,不满足做饭的条件
//让厨师线程等待,等待之前先解锁,被唤醒之后再加锁
//pthread_cond_wait接口中就完成了解锁,休眠,被唤醒后加锁三部操作
//并且解锁和休眠操作是一步完成,保证原子操作
pthread_cond_wait(&cook_cond, &mutex);
}
bowl = 1; //能够走下来表示没饭,bowl==0, 则做一碗饭,将bowl修改为1
printf("I made a bowl of rice\n");
//唤醒顾客吃饭
pthread_cond_signal(&customer_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *thr_customer(void *arg)
{
while(1) {
//加锁
pthread_mutex_lock(&mutex);
while (bowl != 1) { // 没有饭,不满足吃饭条件,则等待
//没有饭则等待,等待前先解锁,被唤醒后加锁
pthread_cond_wait(&customer_cond, &mutex);
}
bowl = 0; // 能够走下来表示有饭 bowl==1, 吃完饭,将bowl修改为0
printf("I had a bowl of rice. It was delicious\n");
//唤醒厨师做饭
pthread_cond_signal(&cook_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t cook_tid[4], customer_tid[4];
int ret, i;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cook_cond, NULL);
pthread_cond_init(&customer_cond, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(cook_tid[0], NULL);
pthread_join(customer_tid[0], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cook_cond);
pthread_cond_destroy(&customer_cond);
return 0;
}
优点:解耦合、支持忙闲不均、支持开发
实现:一个场所(线程安全的缓冲区- - - 数据队列),两种角色(生产者和消费者),三种关系(实现线程安全)。
#include
#include
#include
#include
#include
#define QUEUE_MAX 5
class RingQueue{
public:
RingQueue(int maxq = QUEUE_MAX):_queue(maxq), _capacity(maxq),
_step_read(0), _step_write(0){
//sem_init(信号量, 进程/线程标志, 信号量初值)
sem_init(&_lock, 0, 1);//用于实现互斥锁
sem_init(&_sem_data, 0, 0);//数据空间计数初始为0
sem_init(&_sem_idle, 0, maxq);//空闲空间计数初始为节点个数
}
~RingQueue(){
sem_destroy(&_lock);
sem_destroy(&_sem_data);
sem_destroy(&_sem_idle);
}
bool Push(int data){
//1. 判断是否能够访问资源,不能访问则阻塞
sem_wait(&_sem_idle);//-空闲空间计数的判断,空闲空间计数-1
//2. 能访问,则加锁,保护访问过程
sem_wait(&_lock);//lock计数不大于1,当前若可以访问则-1,别人就不能访问了
//3. 资源的访问
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;//走到最后,从头开始
//4. 解锁
sem_post(&_lock);//lock计数+1,唤醒其它因为加锁阻塞的线程
//5. 入队数据之后,数据空间计数+1,唤醒消费者
sem_post(&_sem_data);
return true;
}
bool Pop(int *data){
sem_wait(&_sem_data);//有没有数据
sem_wait(&_lock); // 有数据则加锁保护访问数据的过程
*data = _queue[_step_read]; //获取数据
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock); // 解锁操作
sem_post(&_sem_idle);//取出数据,则空闲空间计数+1,唤醒生产者
return true;
}
private:
std::vector<int> _queue; // 数组, vector需要初始化节点数量
int _capacity; // 这是队列的容量
int _step_read; // 获取数据的位置下标
int _step_write;//写入数据的位置下标
//这个信号量用于实现互斥
sem_t _lock ;
//这个信号量用于对空闲空间进行计数
//---对于生产者来空闲空间计数>0的时候才能写数据 --- 初始为节点个数
sem_t _sem_idle;
// 这个信号量用于对具有数据的空间进行计数
// ---对于消费者来说有数据的空间计数>0的时候才能取出数据 -- 初始为0
sem_t _sem_data;
};
void *thr_productor(void *arg)
{
//这个参数是我们的主线程传递过来的数据
RingQueue *queue = (RingQueue*)arg;//类型强转
int i = 0;
while(1) {
//生产者不断生产数据
queue->Push(i);//通过Push接口操作queue中的成员变量
printf("productor push data:%d\n", i++);
}
return NULL;
}
void *thr_customer(void *arg)
{
RingQueue *queue = (RingQueue*)arg;
while(1) {
//消费者不断获取数据进行处理
int data;
queue->Pop(&data);
printf("customer pop data:%d\n", data);
}
return NULL;
}
int main()
{
int ret, i;
pthread_t ptid[4], ctid[4];
RingQueue queue;
for (i = 0; i < 4; i++) {
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
if (ret != 0) {
printf("create productor thread error\n");
return -1;
}
ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
if (ret != 0) {
printf("create productor thread error\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}
多线程的并发- - -操作系统层面的轮询调度(或者CPU资源足够情况下的并行)
生产者与消费者,其实是两种业务处理的线程而已,我们创建线程就可以,实现的关键在于线程安全队列。
封装一个线程安全的BlockQueue- - -阻塞队列- - -向外提供线程安全的入队/出队操作
class BlockQueue
{
public:
BlockQueue(); //编码风格:纯输入参数 -const& /输出型参数 指针/输出入输出型 &(引用)
bool Push(const int &data); //入队数据
bool Pop(int *data); //出队数据
private:
std::queue<int> _queue; //STL中queue容器,是非线程安全的---因为STL设计之初就是奔着性能去的,并且功能多了耦合度就高了
int _capacity; //队列中节点的最大数量---数据也不能无限制添加,内存耗尽程序就崩溃了
pthread_mutex_t_mutex;
pthread_cond_t_productor_cond; //生产者队列
pthread_cond_t_customer_cond; //消费者队列
}
注意事项:
1、条件变量需要搭配互斥锁一起使用,prhread_cond_wait 集合了解锁/休眠/被唤醒后加锁的散步操作;
2、程序员在程序中对访问条件是否满足的判断需要使用while 循环进行判断;
3、在同步实现时,多种不同的角色线程需要使用多个条件变量,不要让所有的线程等待在一个条件变量上。
可以用于实现进程 / 线程间同步与互斥,信号本质就是一个计数器 + pcb等待队列
同步的实现:通过自身的计数器对资源进行计数,并且通过计数器的资源计数,判断进程/线程是否能够符合访问资源的条件,若符合则可以访问,若不符合则调用提供的接口使进程/线程阻塞;其他进程/线程促使条件满足后,可以唤醒pcb等待队列上的pcb。
互斥的实现:保证计数器的技术不大于1,就保证了资源只有一个,同一时间只有一个进程/线程能够访问资源,实现互斥。
代码的操作
通过信号量实现一个生产者与消费者模型 - - -线程安全的阻塞队列
使用数组实现一个队列
队列:满足先进先出特性的容器都是队列
class RingQueue
{
std::vector<int>_queue; //数组
int _capcity; //这是队列的容量
int _strp_read; //获取数据的位置下标
int _step_write; //写入数据的位置下标
sem_t lock; //这个信号量用于实现互斥
sem_t_sem_idle; //这个信号量用于对空闲空间进行计数,对于生产者来空闲空间计数>0的时候才能写数据(初始化为节点个数)
sem_t_sem_data; //这个信号量用于对具体数据的空间进行计数,对于消费者来说有数据的空间计数 >0 的时候才能取出数据(初始值为0)
}
线程池:线程的池子,有很多线程,但是数量不会超过池子的限制。需要用到多执行流进行任务处理的时候,就从池子中取出一个线程去处理。
应用场景:由大量的数据处理请求,需要多执行流并发/并行处理。
若是一个数据请求的到来伴随着一个线程的创建去处理,则会产生一些风险以及一些不必要的损耗:
自主编写一个线程池:大量线程(每个线程中都是进行循环任务处理)+ 任务缓冲队列
线程的入口函数,都是在创建线程的时候,就固定传入,导致线程池中的线程进行任务处理的方式过于单一;
因为线程的入口函数都是一样的,处理流程也都是一样的,只能处理单一方式的请求,灵活性太差;
若任务队列中的任务,不仅仅是单纯的数据,而是包含任务处理方法在内的数据,这时候,线程池中的线程只需要使用传入的方法,处理传入的数据即可,不需要关心是什么数据,如何处理,提高线程池的灵活性。
线程池就类似于一个实现了消费者业务的生产者与消费者模型
**每个线程的入口函数中,只需要不断的获取任务节点,调用任务节点中Run接口就可以实现处理了。
typedef void(*_handler)(int data);
class MyTask{
public:
SteTask(int data, handler_t handler); //用户自己传入要处理的数据和方法,组织出一个任务节点
Run(){ return_handler(_data);}
private:
int _data; //要处理的数据
handler_t_handler; //处理数据的方法
}
class ThreadPool{
int thr_max; //定义线程池中线程的最大数量,初始化时创建相应数量的线程即可
std::queue<MyTask>_queue;
pthread_mutex_t _mutex; //实现_queue操作的安全性
pthread_cond_t _cond; //实现线程池中消费者线程的同步
}
要处理什么数据,什么样处理的方法,组织成一个任务节点,交给线程池,线程中找出任意一个线程只需要使用方法处理数据即可。
STL中的容器都是线程安全的吗? - - - - - 不是
只能指针是线程安全的吗? - - unique_ptr 因为局部操作/ shared_ptr 原子操作,不涉及线程安全的问题
线程池.hpp 程序:
#include
#include
#include
#include
#include
typedef void (*handler_t)(int);
class ThreadTask
{
public:
ThreadTask(){
}
void SetTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
void Run() {//外部只需要调用Run,不需要关系任务如何处理
return _handler(_data);
}
private:
int _data;//任务中要处理的数据
handler_t _handler;//任务中处理数据的方法
};
#define MAX_THREAD 5
class ThreadPool
{
public:
ThreadPool(int max_thr = MAX_THREAD):_thr_max(max_thr){
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond, NULL);
for (int i = 0; i < _thr_max; i++) {
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, this);
if (ret != 0) {
printf("thread create error\n");
exit(-1);
}
}
}
~ThreadPool(){
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
bool TaskPush(ThreadTask &task) {
pthread_mutex_lock(&_mutex);
_queue.push(task);
pthread_mutex_unlock(&_mutex);
pthread_cond_broadcast(&_cond);//如对后唤醒所有线程,谁抢到谁处理
return true;
}
// 类的成员函数,有一个隐藏的默认参数,是this指针
// 线程入口函数,没有this指针,如何操作私有成员呢??
static void *thr_start(void *arg) {
ThreadPool *p = (ThreadPool *) arg;
//不断的从任务队列中取出任务,执行任务的Run接口就可以
//每一个任务节点中包含了要处理的数据,以及如何处理的函数
while(1) {
pthread_mutex_lock(&p->_mutex);
while(p->_queue.empty()) {
pthread_cond_wait(&p->_cond, &p->_mutex);
}
ThreadTask task;
task = p->_queue.front();
p->_queue.pop();
pthread_mutex_unlock(&p->_mutex);
task.Run();//任务的处理要放在解锁之外,因为当前的所保护的时队列的操作
}
return NULL;
}
private:
int _thr_max; // 线程池中线程的最大数量--根据这个初始化创建指定数量的线程
std::queue<ThreadTask> _queue;
pthread_mutex_t _mutex;//保护队列操作的互斥锁
pthread_cond_t _cond;//实现从队列中获取节点的同步条件变量
};
线程池 main 程序:
#include
#include "threadpool.hpp"
void test_func(int data)
{
int sec = (data % 3) + 1;
printf("tid:%p -- get data:%d , sleep:%d\n", pthread_self(), data, sec);
sleep(sec);
}
void tmp_func(int data) {
printf("tid:%p -- tmp_func\n", pthread_self());
sleep(1);
}
int main()
{
ThreadPool pool;
for(int i = 0; i < 10; i++) {
ThreadTask task;
if (i % 2 == 0) {
task.SetTask(i, test_func);
}else {
task.SetTask(i, tmp_func);
}
pool.TaskPush(task);
}
sleep(1000);
return 0;
}
单例模式:是一种典型常用的一种设计模式,一份资源只能被申请加载一次 / 单例模式的方法创建的类在当前进程中只有一个实例。
实现方式:
实现过程中需注意的细节