多线程
线程:
**概念:**线程就是进程的内部的一个控制序列,通俗的说就是程序的一个执行线路.一切进程都至少有一个线程,线程在进程内部运行的本质是在进程地址空间内运行
线程在Linux下的描述组织:
Linux下pcb是一个线程;因为Linux下线程以进程的PCB模拟实现线程,所以Linux下线程也叫轻量级进程
进程就是线程组;在Linux下线程是CPU调度的基本单位,进程是资源分配的资源的基本单位,多线程可以并行多任务,进程也可以
线程并行多任务的优点和缺点:
优点:进程中的线程公用同一个地址空间;线程间通信方便;创建销毁成本更低;线程切换调度成本更低;执行粒度更加细致
**缺点:**线程缺乏访问控制:健壮性低----系统调用(exit),异常针对整个进程,健壮性低,性能损失
多进程/多线程进行任务处理的优势和细节体现
CPU密集型:
io密集型:
进程/线程避免栈混乱的方式
栈混乱的成因进程/新城使用同一个虚拟地址空间
栈混乱
栈的作用:
当调用一个函数的时候就会将函数地址压入栈中(当然还有函数中的局部变量数据),表示这就是现在正在运行的函数;等到函数运行完,这时候这个函数出栈,那么继续要运行的函数位置就是从栈顶取出的函数 比如:在A函数中调用函数B,先将A压栈,然后将B压栈,等待B运行完了出栈,这是取出栈顶就知道继续顺着A往下运行
栈混乱:
而栈混乱举个例子:vfork创建子进程,两个进程共享一个地址空间,共用一个栈;如果两个同时运行调用各种函数就会往栈中压入各自的函数,但是等出栈的时候就混乱了,不知道谁是谁的函数…
**进程:**vfork创建一个子进程公用同一个地址空间,怕出现栈混乱,因此只有子进程运行完毕的时候或程序替换的时候父进程才开始运行
**多线程:**每个线程虚拟地址空间单独分配一块空间,每个线程都会有一些独立的信息(栈,一套寄存器,上下文数,errno,信号屏蔽字-------自己的block集合)
**线程的共享数据:**代码段,数据段,文件描述符表
线程控制
线程ID和进程ID
线程组:
多线程的进程又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述(task_struct) 与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是线程组ID,该值对应的是进程ID;
线程组ID:
线程组内的第一 个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以 线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程直 接创建线程,还是创建出来的线程再次创建线程,都是这样。
ps -L:查看线程信息
tid | task_struct_pid | tgid |
---|---|---|
线程地址空间首地址 | LWP:当前线程ID | pid=主线程id |
线程创建
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查
pthreads函数出错时不会设置全局变量errno,pthreads提供了errno变量,但对于pthreads函数的错误,
建议通过返回值判定,因为系统的开销更小
错误码:errno
Linux中系统调用的错误都存储于 errno
中,errno
由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。
POSIX:
POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准
线程终止
主线程退出进程不会退出,
线程入口函数中return ,main中不能return 因为会退出进程
线程退出也会成为僵尸线程(普通线程不体现)线程内存地址空间无法回收,主线程退出进程不会退出
pthread_exit 主动退出
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread——cancel 取消线程被动退出
功能:
取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:
成功返回0;失败返回错误码
线程等待
概念:获得指定线程退出的返回值并且允许操作系统回收线程资源.
功能:
等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:
成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终 止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的value_ptr所指向的单元存放的是传给pthread_exit的参数
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
线程分离
一个线程启动默认有一个属性处于joinable的状态,处于joinable状态的线程,退出后不会自动释放资源,需要其他线程等待phread_join的返回值
int pthread_detach(pthread_t thread);
参数:
要被分离的线程ID
功能:
分离一个线程(设置线程的属性从joinable---->到detach属性)线程退出后将自动回收资源
被分离的线程无法被等待,若是非要pthread_join则会直接报错返回
分离的前提;不关心返回值,线程默认属性是joinable
线程安全:
概念:线程安全的意义:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
- 多个线程同时处理操作临界资源不会出现数据二义性:
- 在线程中是否对临界资源进行非原子操作
- 可重入与不可重入:多个执行流中是否可以同时进入函数运行而不出现问题;
实行线程安全:同步互斥
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
同步:临界资源的时序可控
互斥:临界资源的唯一访问
可重入与不可重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
可重入与线程安全的辨析
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的。
互斥的实现原理:原子操作的计数器
互斥锁:
一个0/1的计数器—1表示可以加锁;操作完毕后解锁解锁就是计数+1; 0表示不可以使用加锁等待
互斥锁操作步骤:
1.定义互斥量: pthread_mutex_t
2.初始化互斥锁变量:
1.静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2.函数接口动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:互斥锁变量
attr:属性一般置空
注意:
加锁要在临界资源访问之前
互斥锁不一定是全局变量:只要互斥的
3.加锁/解锁:pthread_mutex_lock
int pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞加锁,加不上锁阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
非阻塞加锁:加不上锁报错
timelock:限 时加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
4.临界资源操作完毕解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
5.销毁互斥锁:
销毁互斥量需要注意:
-
使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
-
不要销毁一个已经加锁的互斥量
-
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
死锁
因为加锁之后没有释放导致程序卡死,永久等待的状态(对资源的竞争以及线程进程推进的顺序不当
死锁产生的必要条件:
-
互斥条件
-
不可剥夺条件
-
请求与保持条件
-
循环等待条件
死锁的避免
加锁顺序一致 避免锁未释放的场景 资源一次性分配
产生场景:
线程同步实现:
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件
条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量的实现步骤:
1.定义条件变量:pthread_cond_t
2.初始化条件变量:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
cond:条件变量;
attr:属性 一般置NULL
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
3.等待条件:
限时等待:返回等待超时
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
cond 条件变量
mutex:互斥锁
abtime:等待时长
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex)
参数:
cond:要在这个条件变量上等待
mutex:互斥量
集合了解锁后的挂起操作(原子操作)被唤醒后加锁
pthread_ cond_ wait 中互斥量的作用
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须 要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件 变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有 互斥锁就无法安全的获取和修改共享数据。
解锁后挂起操作为什么是原子操作
由于解锁和等待不是原子操作。调用解锁之后,pthread_ cond_ wait之前,如果已经有其他线程获取到互斥 量,摒弃条件满足,发送了信号,那么pthread_ cond_ wait将错过这个信号,可能会导致线程永远阻塞在这 个pthread_ cond_ wait。所以解锁和等待必须是一个原子操作。
*int pthread_cond_wait(pthread_cond_ t cond,pthread_mutex_ t * mutex)的工作原理
进入该函数后,会去看条件 量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
4.唤醒:
pthread_cond_broadcast/pthread_cond_signal
5.销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
条件变量的注意事项
因为条件变量提供等待唤醒的功能,具体什么时候等待需要用户判断这个条件判断通常涉及临界资源的操作(其他线程需要通过线程要通过修改条件,来促使条件满足)而这个临界资源的操作应该受保护所以要搭配互斥锁
线程有多少角色就要有多少个条件变量
实例
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//临界资源
int have_noodle = 1;
pthread_cond_t boss;
pthread_cond_t customer;
pthread_mutex_t mutex;
void *thr_boss(void* arg){
while(1){
pthread_mutex_lock(&mutex);
//
while(have_noodle == 1){
pthread_cond_wait(&boss,&mutex);
}
printf("i make it+1\n");
have_noodle+=1;
pthread_cond_signal(&customer);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_customer(void* arg){
while(1){
pthread_mutex_lock(&mutex);
while(have_noodle == 0){
pthread_cond_wait(&customer,&mutex);
}
printf("i have ate it\n");
have_noodle-=1;
pthread_cond_signal(&boss);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(int argc ,char* argv[]){
pthread_t tid1,tid2;
int ret;
pthread_cond_init(&boss,NULL);
pthread_cond_init(&customer,NULL);
pthread_mutex_init(&mutex,NULL);
int i = 0;
for(i = 0; i< 2;++i){
ret =pthread_create(&tid1,NULL,thr_boss,NULL);
if(ret != 0){
printf("boss error\n");
}
}
for(i = 0; i< 2;++i){
ret =pthread_create(&tid2,NULL,thr_customer,NULL);
if(ret != 0){
printf("boss error\n");
}
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_cond_destroy(&boss);
pthread_cond_destroy(&customer);
pthread_mutex_destroy(&mutex);
return 0;
}
注意:
使用while不使用if的原因:不同线程的临界资源的多次操作
使用多个条件判断避免卡死
生产者消费者模型:
两个角色之间的关系
生产者与生产者互斥
消费者与消费者互斥
生产者与消费者同步+互斥
生产者消费者模型代码:
一个场所:线程安全队列
两个角色:cond_product cond_consumer
三种关系:
线程安全队列
#include <iostream>
#include <queue>
#include <pthread.h>
#define CAPACITY 10
class BlockQueue{
public:
BlockQueue(){
_capacity = 10;
pthread_mutex_init(&_mutex,NULL);
pthread_cond_init(&_cond_productor,NULL);
pthread_cond_init(&_cond_consumer,NULL);
}
~BlockQueue(){
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool Queuepush(int data){
QueueLock();
while(Queueisfull()){
ProductorWait();
}
_quene.push(data);
ConsumerWeakup();
Queueunlock();
return true;
}
bool QueuePop(int& data){
QueueLock();
while(Queueisenpty()){
ConsumerWait();
}
data = _quene.front();
_quene.pop();
ProductorWeakup();
Queueunlock();
return true;
}
private:
void QueueLock(){
pthread_mutex_lock(&_mutex);
}
void Queueunlock(){
pthread_mutex_unlock(&_mutex);
}
void ProductorWait(){
pthread_cond_wait(&_cond_productor,&_mutex);
}
void ConsumerWait(){
pthread_cond_wait(&_cond_consumer,&_mutex);
}
void ProductorWeakup(){
pthread_cond_signal(&_cond_productor);
}
void ConsumerWeakup(){
pthread_cond_signal(&_cond_consumer);
}
bool Queueisfull(){
int size_queue = _quene.size();
return (size_queue ==_capacity);
}
bool Queueisenpty(){
return _quene.empty();
}
private:
std::queue<int> _quene;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _cond_productor;
pthread_cond_t _cond_consumer;
};
void *thr_comsumer(void* arg){
BlockQueue* q = (BlockQueue*)arg;
while(1){
int data;
q->QueuePop(data);
std::cout<<"customer"<<data<<std::endl;
}
}
void *thr_producot(void* arg){
BlockQueue* q = (BlockQueue*)arg;
int i = 0;
while(1){
std::cout<<"productor put data"<<i<<std::endl;
q->Queuepush(i++);
}
}
int main(){
pthread_t ctid[4],ptid[4];
BlockQueue q;
for(int i = 0;i < 4;i++){
pthread_create(&ctid[i],NULL,thr_comsumer,(void*)&q);
}
for(int i = 0;i < 4;i++){
pthread_create(&ptid[i],NULL,thr_producot,(void*)&q);
}
for(int i = 0;i < 4;++i){
pthread_join(ctid[i],NULL);
}
for(int i = 0;i < 4;++i){
pthread_join(ptid[i],NULL);
}
return 0;
}
生产者消费者模型功能
-
解耦合
-
支持忙闲不均
-
支持并发
信号量 计数器+等待队列+等待+唤醒
功能:实现线程/进程间的同步和互斥
计数器是判断条件-----计数可用资源的的数目
等待队列+等待+唤醒 实现同步
POSIX信号量
原理:数据操作前进行资源判断:
计数>0计数-1,直接返回
计数 <0 计数-1,阻塞等待
生产数据后计数+1,唤醒等待
初始化信号量:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:
0表示线程间共享,非零表示进程间共享
value:信号量初始值
等待信号量:
功能:
等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);
发布信号量:
功能:
发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);
信号量的销毁:
int sem_destroy(sem_t *sem);
实例
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include <semaphore.h>
#include <semaphore.h>
class RingQueue{
public:
RingQueue(int cap = 10)
:_queue(10),
_write_step(0),
_read_step(0),
_capacity(cap)
{
sem_init(&_sem_data,0,0);
sem_init(&_sem_idle,0,10);
sem_init(&_sem_lock,0,1);
}
~RingQueue(){
sem_destroy(&_sem_data);
sem_destroy(&_sem_idle);
sem_destroy(&_sem_lock);
}
bool QueuePush(int data){
ProductorWait();
QueueLock();
_queue[_write_step] = data;
_write_step = (_write_step+1)%_capacity;
QueueNulock();
ConsumerWeakup();
return true;
}
bool QueuePop(int& data){
ConsumerWait();
QueueLock();
data = _queue[_read_step];
_read_step = (_read_step+1)%_capacity;
QueueNulock();
ProductorWeakup();
return true;
}
private:
void QueueLock(){
sem_wait(&_sem_lock);
}
void QueueNulock(){
sem_post(&_sem_lock);
}
void ProductorWait(){
sem_wait(&_sem_idle);
}
void ProductorWeakup(){
sem_post(&_sem_idle);
}
void ConsumerWait(){
sem_wait(&_sem_data);
}
void ConsumerWeakup(){
sem_post(&_sem_data);
}
private:
std::vector<int> _queue;
int _write_step;
int _read_step;
int _capacity;
sem_t _sem_data;
sem_t _sem_idle;
sem_t _sem_lock;
};
void *thr_comsumer(void* arg){
RingQueue* q = (RingQueue*)arg;
while(1){
int data;
q->QueuePop(data);
std::cout<<"customer"<<data<<std::endl;
}
}
void *thr_producot(void* arg){
RingQueue* q = (RingQueue*)arg;
int i = 0;
while(1){
std::cout<<"productor put data"<<i<<std::endl;
q->QueuePush(i++);
}
}
int main(){
pthread_t ctid[4],ptid[4];
RingQueue q;
for(int i = 0;i < 4;i++){
pthread_create(&ctid[i],NULL,thr_comsumer,(void*)&q);
}
for(int i = 0;i < 4;i++){
pthread_create(&ptid[i],NULL,thr_producot,(void*)&q);
}
for(int i = 0;i < 4;++i){
pthread_join(ctid[i],NULL);
}
for(int i = 0;i < 4;++i){
pthread_join(ptid[i],NULL);
}
return 0;
}
线程池
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个 线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不 仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内 存、网络sockets等的数量