多线程
线程概念
线程是什么?
在一个程序里的一个执行路线就叫做线程(thread)。线程是一个进程内部的控制序列。一切进程至少都有一个执行线程。
进程和线程
进程是资源分配的基本单位
线程是资源调度的最小单位
线程共享进程数据,但也拥有自己的一部分数据:线程ID>一组寄存器>栈>errno>信号屏蔽字>调度优先级
进程和线程的关系:
线程之间的独有与共享
独有:栈,寄存器,信号屏蔽字,errno,线程ID
共享:虚拟地址空间(数据段/代码段),文件描述符,信号处理方式,当前工作目录,用户id/组id
进程和线程的区别
进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
多线程与多进程任务处理的优缺点分析
优点
- 创建一个新线程的代价比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速IO操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- IO密集型应用,为了提高性能,将IO操作重叠。线程可以同时等待不同的IO操作。
缺点
- 性能损失
- 健壮性降低
- 缺乏访问控制
- 编程难度提高
线程控制
线程控制分为:线程创建/线程终止/线程等待/线程分离
线程控制的接口都是库函数实现的:创建一个用户态线程让用户控制,但是程序的调度处理都是通过轻量级进程pcb实现的。
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些库函数,要通过引入头文件<pthread.h>
- 链接这些线程函数库时,要使用编译器命令的"-lpthread"选项
线程创建
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
参数:
thread:返回线程ID--线程地址空间在整个虚拟地址空间中的首地址
attr:设置线程的属性,为NULL表示使用默认属
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0,失败返回错误码
线程id:
tid-线程地址空间首地址-方便用户操作线程
pcb->pid 轻量级进程ID–LWP
pcb->tgid 线程组ID-进程ID == 首线程的pid
线程终止
return pthread_exit(void *retval) pthread_cancel(pthread_t tid)
线程等待
等待指定线程退出,获取这个线程的退出返回值,并且回收这个线程的资源;
一个线程有一个默认属性:joinable;处于joinable属性的线程退出后为了保存返回值,因此不会自动释放资源;如果不进行等待则会造成资源泄露
一个线程也只有处于joinable状态的时候,才需要被等待。
pthread_join(pthread_t tid,void **retval)
线程分离
线程分离就是将线程joinable属性修改为detach属性
线程若处于detach属性,则线程退出后将自动回收资源;并且这个线程不需要被等待,等待是毫无意义的,因为线程退出返回值占用的空间已经被回收了
pthread_detach(pthread_t tid)
线程分离的适用场景:
一进入自己的线程就线程等待:pthread_detach(pthread_self);
线程安全
多个线程同时对临界资源进行访问而不会造成数据的二义性
同步:时序合理性
互斥:对临界资源访问的唯一性
线程间互斥的实现:互斥锁(只有0/1的原子操作的计数器 + 等待队列)
-
定义互斥锁变量 pthread_mutex_t
-
对互斥锁变量进行初始化 pthread_mutex_init(&mutex,&attr) 或者 赋值初始化
-
对临界资源操作之前先加锁 pthread_mutex_lock(&mutex)
若可以加锁则直接修改计数,函数返回;否则挂起等待
pthread_mutex_trylock / pthread_mutex_timedlock
-
对临界资源操作完成后进行解锁
pthread_mutex_unlock(&mutex); -
销毁互斥锁
pthread_mutex_destory(&mutex);
#include <pthread>
int ticket = 100;
void *yellow_bull(void *arg)
{
while(1)
{
//int pthread_mutex_trylock(pthread_mutex_t *mutex);
//若不能加锁,立即报错返回
//int pthread_mutex_lock(pthread_mutex_t *mutex);
// _mutex:互斥锁变量
pthread_mutex_lock();
if(ticket > 0 )
{
printf("bull %d get a ticket:%d\n",(int)arg,ticket);
ticket--;
}
else
{
printf("have no tickets,bull %d exit\n",(int)arg);
//用户加锁之后,需要在任意有可能退出线程的地方进行解锁
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
//int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid[4];
//
// mutex 互斥锁变量
//
pthread_mutex_init(&mutex,NULL);
int i;
for(i=0 ;i < 4;i++)
{
int ret = pthread_creat(&tid[i],NULL,yellow_bull,(void*)i);
if(ret != 0)
{
printf("thread craete error\n");
return -1;
}
}
for (int i = 0;i < 4;i++)
pthread_join(tid[i],NULL);
return 0;
}
死锁
多个线程对锁资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使程序无法往下运行。、
死锁产生的四个必要条件:
- 互斥条件 一个锁只有一个线程可以获取
- 不可剥夺条件 我加的锁别人不能解
- 请求与保持条件 拿着A锁 去请求B锁,但是获取不到B锁,也不释放A锁
- 环路等待条件 我拿着A锁请求B锁,对方拿着B锁请求A锁
死锁预防: 破坏四个必要条件
死锁避免: 死锁检测算法,银行家算法
线程间同步的实现
等待 + 唤醒
操作条件不满足则等待,别人促使条件满足后唤醒等待
条件变量:
条件变量实现同步:
线程在对临界资源访问之前,先判断是否能够操作;若可以操作则线程直接操作;
否则若不能操作;则条件变量提供等待功能;让pcb等待在队列上
其他线程促使操作条件满足,然后唤醒条件变量等待队列上的线程
-
定义条件变量 pthread_cond_t
-
条件变量初始化 pthread_cond_init(&cond,&attr);
-
用户在判断条件不满足的情况下提供等待功能 pthread_cond_wait(&cond,&mutex)/pthread_cond_timewait();
为什么条件变量要与互斥锁一起使用:线程什么时候等待,需要一个判断条件;而这个判断条件也是一个临界资源(等待了之后,其他线程需要促使这个条件满足(修改这个临界资源));因此这个临界资源的操作就需要受保护(默认使用互斥锁实现保护)
-
用户在促使条件满足后,唤醒等待 pthread_cond_signal(&cond)(至少唤醒一个) / pthread_cond_broadcast(&cond)
-
销毁条件变量pthread_cond_destory(&cond)
//实现条件变量的基本使用 //以自己吃面和厨师做面为例子: //自己想要吃面,前提是有面,没有面则等待 //厨师做面,做好面后,唤醒吃面的人 #include<stdio.h> #include<unistd.h> #include<pthread.h> int _have_noodle = 0; pthread_mutex_t mutex; pthread__cond_t cond_cook; pthread__cond_t cond_eat; void *eat_noodle(void *arg) { while(1) { pthread_mutex_lock(&mutex); while(_have_noodle == 0) { //没有面,就不能吃面 //usleep(1000); //continue; //int pthread_cond_timedwait(pthread_cond_t *restrict cond, // pthread_mutex_t *restrict mutex, // const struct timespec *restrict abstime); //限时等待,等待超时后报错返回 //int pthread_cond_wait(pthread_cond_t *restrict cond, // pthread_mutex_t *restrict mutex); //一直阻塞等待 // pthread_cond_wait实现三步: //1.解锁 //2.休眠 //3.被唤醒后加锁 //其中解锁和休眠操作必须是原子操作 pthread_cond_wait(&cond_eat,&mutex); } //能走下来表示have_noodle==1,表示有面 printf("eat noodle,decilious\n"); _have_noodle = 0; pthread_cond_signal(&cond_cook); } } void *cook_noodle(void *arg) { while(1) { while(_have_noodle == 1) { //现在有面,但是没人吃,不能继续做了 // continue; pthread_cond_wait(&cond_cook,&mutex); } printf("cook nooldle,come on\n"); _have_noodle = 1; pthread_cond_signal(&cond_eat); pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t tid1,tid2; pthread_mutex_init(&mutex,NULL); pthread_cond_init(&cond_eat,NULL); pthread_cond_init(&cond_cook,NULL); for(int i = 0;i < 4;i++) { int ret = pthread_create(&tid1,NULL,eat_noodle,NULL); if(ret != 0) { printf("pthread create error\n"); return -1; } } for(int i = 0;i < 4;i++) { int ret = pthread_create(&tid2,NULL,eat_noodle,NULL); if(ret != 0) { printf("pthread create error\n"); return -1; } } pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond_eat); pthread_cond_destroy(&cond_cook); return 0; }
条件变量的条件判断应该是一个循环判断:
多个顾客线程若同时被唤醒,只有一个顾客可以加锁,其他的顾客线程将阻塞在加锁上(而不是条件变量的等待);
第一个加锁的顾客开始吃面,吃碗面后进行解锁,这时候,获取到锁的线程有可能是一个顾客线程,因为再次判断有没有面,因此直接吃面,但是面已经被第一个顾客线程吃掉了,因此逻辑错误应该加锁之后重新再次判断是否有面。
不同角色应该等待在不同的条件变量上:
在有多个厨师线程和顾客线程的时候,若是顾客和厨师线程都等待在同一个条件变量的等待队列中,会导致厨师做了一碗面,本应该唤醒顾客线程吃面,但是这时候有可能唤醒的是一个厨师线程,而厨师线程因为循环判断有没有面,因为已经有面而陷入等待(而顾客线程因为没有被唤醒而无法吃面)
生产者与消费者模型
一个场所,两种角色,三种关系
优点(解决的问题):
- 解耦
- 支持并发
- 支持忙闲不均
这些优点都是通过这个场所来提供的,但是因为多个角色有可能同时操作场所,因此要保证场所的操作安全。
三种关系:
- 生产者与生产者互斥关系;
- 消费者与消费者互斥关系;
- 生产者与消费者具有同步+互斥的关系;
BlockQueue
#include<iostream>
#include<queue>
#include<pthread.h>
#define MAX_QUEUE 10
class BlockQueue
{
private:
std::queue<int> _queue;
int _capacity;//用于限制队列中最大节点数量
pthread_mutex_t _mutex;
pthread_cond_t _cond_product;
pthread_cond_t _cond_consumer;
public:
BlockQueue(int cap = MAX_QUEUE):_capacity(cap)
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond_eat,NULL);
pthread_cond_init(&cond_cook,NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_eat);
pthread_cond_destroy(&cond_cook);
}
bool QueuePush(int data)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_product,&_mutex);
}
_queue.push(data);
pthread_cond_signal(&_cond_consumer);
pthread_mutex_unlock(&_mutex);
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
pthread_cond_wait(&_cond_consumer, &_mutex);
}
data = _queue.front();
_queue.pop();
pthread_cond_signal(&_cond_product);
pthread_mutex_unlock(&_mutex);
}
};
void *thr_consumer(void *arg)
{
BlockQueue *q = (BlockQueue*)arg;
while(1)
{
int data;
q->QueuePop(data);
std::cout<<"consumer:"<<pthread_self()<<"get data:"<<data<<"\n";
}
return NULL;
}
void *thr_product(void *arg)
{
BlockQueue *q = (BlockQueue*)arg;
int i =0;
while(1)
{
q->QueuePush(i++);
std::cout<<"product:"<<pthread_self()<<"put data:"<<i<<"\n";
}
return NULL;
}
int main()
{
pthread_t con_tid[4],pro_tid[4];
BlockQueue q;
for(int i = 0;i < 4;i++)
{
int ret = pthread_create(&con_tid[i],NULL,thr_consumer,(void*)&q);
if(ret != 0)
{
std::cout<<"pthread create error\n";
return -1;
}
}
for(int i = 0;i < 4;i++)
{
int ret = pthread_create(&cpro_tid[i],NULL,thr_product,(void*)&q);
if(ret != 0)
{
std::cout<<"pthread create error\n";
return -1;
}
}
for(int i = 0;i < 4;i++)
{
pthread_join(con_tid[i],NULL);
}
for(int i = 0;i < 4;i++)
{
pthread_join(pro_tid[i],NULL);
}
return 0;
}
信号量
功能:实现线程间同步互斥
本质:一个计数器(做资源计数-判断当前是否可以对临界资源进行操作)+ 等待队列 + 等待 + 唤醒
原理:
互斥原理:只具有0/1计数时,就可以实现互斥
初始计数为1,1表示当前只有一个线程能够获取资源,获取资源之后-1;
计数+1:并且唤醒等待队列上的线程;
计数为0,其他线程则进行等待,(将pcb加入到等待队列)
同步原理:对程序逻辑进行控制(对临界资源合理操作控制);通过计数判断当前是否能够对临界资源进行 操作,不能操作(计数<=0),则等待;
其他线程促使条件判断后,对计数+1,唤醒等待的线程
条件变量实现同步与信号量的区别:
1.信号量并不需要搭配互斥锁的使用
2.信号量本身的计数就是能够操作临界资源的判断条件
流程:
1.定义信号量:sem_t
2.初始化信号量 int sem_init(sem_t *sem,int pshared,unsigned int value);
sem:信号量变量
pshared:0-用于线程间同步与互斥 !0-用于进程间同步与互斥
value:信号量计数器初始值
3.判断计数是否可以对临界资源进行操作 int sem_wait(sem_t *sem);
sem:信号量变量
对计数进行判断,是否>0;若>0则函数立即正确返回,返回前计数-1,程序流程继续向下可以操作临界资 源;若<=0,则认为当前不能对临界资源进行操作,则线程等待
使用信号量实现生产者与消费者模型:
1.实现线程安全的环形队列
2.
//实现线程安全的环形队列
class RingQueue
{
std::vector<int> _queue;
int _capacity;
int _read;
int _write;
sem_t _lock;
sem_t _idle_space;//生产者入队数据之前判断队列中是否有空闲空间;判断能否入队数据
sem_t _data_space;//消费者获取数据之前判断有数据的空间有多少判断能否获取数据
}
实现:posix
//posix标准的信号量的基本操作
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
int ticket = 100;
sem_t sem;
void *thr_start(void *arg)
{
while(1)
{
if(ticket>0)
{
// int sem_wait(sem_t *sem);
//若当前计数<=0;则线程一直等待
// int sem_trywait(sem_t *sem);
//若当前计数<=0;则线直接报错返回
//int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//若当前计数<=0;等待时间超时后报错返回
sem_wait(&sem)
printf("get a ticket:%d\n",ticket);
ticket--;
}
else
{
pthread_exit(NULL);
}
// int sem_post(sem_t *sem);
//计数+1,促使其他线程
sem_post(&sem);
}
}
int main()
{
int i,ret;
ptthread_t tid[4];
//int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_init(&sem,0,1);
for(int i = 0;i < 4;i++)
{
int ret = pthread_create(tid[i],NULL,thr_start,NULL);
if(ret != 0)
{
printf("pthread create error\n");
return -1;
}
}
for(int i = 0;i < 4;i++)
{
pthread_join(tid[i],NULL);
}
sem_destroy(&sem);
return 0;
}
**********缺
//通过信号量实现生产者消费者模型
class RingQueue
{
std::vector<int> _queue;
int _capacity;
int _read;
int _write;
sem_t _lock;
sem_t _idle_space;//生产者入队数据之前判断队列中是否有空闲空间;判断能否入队数据
sem_t _data_space;//消费者获取数据之前判断有数据的空间有多少判断能否获取数据
public:
RingQueue(int maxque = MAX_QUEUE):_capacity(maxque),
_queue(maxque)
{
sem_init(&_lock,0,1);
sem_init(&_idle_space,0,_capacity);
sem_init(&_data_space,0,0);
}
~RingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_idle_space);
sem_destroy(&_data_space);
}
bool QueuePush(int data)
{
sem_wait(&_idle_space);
_queue.push_back(data);
_write = (_write + 1) % _capacity;
sem_post(&_lock);
sem_post(&_data_space);
return true;
}
bool QueuePop(int &data)
{
sem_wait(&_data_space);//通过数据空间计数判断是否有数据操作
sem_wait(&_lock);
data = _queue[_read];
_read = (_read + 1) % capacity;
sem_post(&_lock);
sem_post(&idle_space);//空闲空间计数+1;唤醒生产者
return true;
}
}
void *productor(void *arg)
{
RingQueue *q = (RingQueue*)arg;
int i = 0;
while(1)
{
std::out << "productor put a data: "<<i<<std::endl;
}
}
线程池
至少一个线程 + 任务队列
在程序初始化时,创建固定数量的线程(最大数据限制),从任务队列中获取任务,进行处理
作用:
1.避免为大量请求创建线程,导致瞬间资源耗尽程序崩溃的问题
2.避免大量线程频繁创建销毁所带来的时间成本
线程的实现
#include <iostream>
#include <queue>
#include <pthread.h>
typedef void (*handler_t)(int data)
class Task
{
public:
void SetTask(int data,handler_t handler)
{
_data = data;
_handler = handler;
}
void Run()
{
_handler(_data);
}
private:
int _data;
handler_t handle;
};
class threadpool
{
public:
ThreadPool(int max):thr_max(max)
{
pthread_mutex_init(&_mutex,NULL);
pthread_cond_init(&_con_cond,NULL);
pthread_cond_init(&_pro_cond,NULL);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_con_cond);
pthread_cond_destroy(&_pro_cond);
}
bool TaskPush(Task &t)
{
pthread_mutex_lock(&_mutex);
_task_queue.push(t);
pthread_cond_signal(&_con_cond);
pthread_mutex_unlock(&_mutex);
return true;
}
bool PoolInit()
{
int ret;
pthread_t tid;
for(int i = 0;i < _thr_max;i++)
{
ret = pthread_create(tid[i],NULL,thr_start,(void*)this);
if(ret != 0)
{
printf("pthread create error\n");
return false;
}
}
_cur_thr++;
pthread_detach(tid);
}
bool TaskPop(Task &t)
{
t = _task_queue.front();
_task_queue.pop();
return true;
}
static void *thr_start(void *arg)
{
ThreadPool *pool = (ThreadPool*)arg;
pthread_mutex_lock(&_mutex);
while(pool->QueueIsEmpty)
{
pthread_cond_wait(&_con_cond,&_mutex);
}
Task t;
TaskPop(t);
pool->QueueUnBlock();
pthread_mutex_unlock(&_mutex);
t.Run();
return NULL;
}
void PoolQuit()
{
if(_quit_flag == false)
while(_cur_thr > 0)
{
Consumer
}
}
public:
void QueueBlock()
{
pthread_mutex_lock(&_mutex);
}
void QueueUnBlock()
{
pthread_mutex_unlock(&_mutex);
}
void ConsumerWait()
{
pthread_cond_wait(&_con_cond,&_mutex);
}
void ConsumerWakeup()
{
pthread_cond_signal(&_con_cond);
}
bool QueueIsEmpty()
{
return _task_quque.empty();
}
private:
int _thr_max;
int _cur_thr;
std::queue<Task> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _pro_cond;
pthread_cond_t _con_cond;
TaskPush(Task &t);
TaskPop(Task &t);
void *thr_start(void *arg)
};
void thr_handle(int data)
{
srand(time(NULL));
int sec = rand() % 5;
std::cout<<"thr:"<<pthread_self()<<"get data:"<<data<<"sleep"<<sec<<" sec\n";
sleep(sec);
}
int main()
{
ThreadPool pool;
Task task;
pool.PoolInit();
for(int i = 0;i < 10;i++)
{
Task task;
task->SetTask(i,thr_handle);
pool.TaskPush(task);
}
pool.PoolQuit();
return 0;
}