Linux多线程
1.线程概念
线程是进程中的一条执行流。
在之前进程的章节,我说进程就是一个pcb,是程序动态运行的描述,通过pcb可以实现操作系统对程序运行的调度管理。
在本章节,我们知道线程是进程中的一条执行流,这个执行流在linux中是通过pcb来实现的,因此linux下的线程就是一个pcb,但是这个pcb共用同一个虚拟地址空间,相比较传统pcb更加轻量化,因此也被称为轻量级进程。
linux下的进程其实就是一个线程组,一个进程中可以有多个线程,线程是进程中的一条执行流。
线程是cpu调度的基本单位/进程是系统资源分配的基本单位。
线程之间的独有与共享
独有:标识符,寄存器,栈,信号屏蔽字,errno,优先级。
共享:虚拟地址空间,文件描述符表,信号处理的回调函数,用户ID/组ID/工作路径。
2.多进程/多线程对比
多进程的优点
1.多进程的健壮性、稳定性更高(异常以及一些系统调用exit直接针对整个进程生效)。
多线程的优点
1.线程间通信更加灵活方便。
除了进程间通信方式之外,还有全局变量以及函数传参(共有同一个虚拟地址空间,只要知道地址就能访问同一块空间)。
2.线程创建于销毁成本更低。
3.同一个进程中的线程间调度成本更低。
共同优点
1.IO密集型程序:多任务并行处理。多磁盘可以实现同时处理。
2.CPU密集型程序:程序中进行大量的数据运算处理。CPU资源足够就可以同时处理。
3.线程控制
线程的创建
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,失败返回非0值。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void* func(void* arg)
{
while(1)
{
printf("%s\n", (char*)arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
char* ptr = "hello";
int ret = pthread_create(&tid, NULL, func, (void*)ptr);
if(ret != 0)
return -1;
while(1)
{
printf("main pthread\n");
sleep(1);
}
return 0;
}
tid是一个线程id,线程的操作句柄,每个线程被创建出来之后,都会开辟一块空间,存储自己的栈和描述信息,这个id其实就是线程独有的这块空间的首地址。
线程的退出
1.线程入口函数运行完毕,线程就会自动退出—在线程入口函数中调用return。
2.线程退出接口。
void pthread_exit(void* retval);
//线程退出接口,谁调用谁退出。retval是退出返回值。
int pthread_cancel(pthread_t thread);
//在主线程中使用,用于指定子线程退出。
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* func(void* arg)
{
while(1)
{
printf("thread\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, func, NULL);
if(ret != 0)
return -1;
pthread_cancel(tid);
while(1)
{
printf("main\n");
sleep(1);
//pthread_exit(NULL);
}
return 0;
}
线程的等待
等待一个线程的退出,获取退出线程的返回值,回收线程所占的资源。
线程有一个属性,默认创建出来这个属性是joinable,处于这个属性的线程退出后需要被其它线程等待获取返回值回收资源。
void pthread_join(pthread_t thread, void** retval);
//等待指定线程退出,获取其返回值,若线程没有退出则一直等待。
thread:要等待退出的线程tid。
retval:输出型参数,用于返回线程的返回值。
int pthread_detach(pthread_t thread);
//线程分离:将线程joinable属性修改为detach属性。
一个线程若是detach属性,那么这个线程退出后则自动释放资源,不需要被等待。
分离一个线程,一定是对该线程的返回值不感兴趣,不想获取到这个返回值,又不想一直等待他的退出,才会分离线程。
pthread_t pthread_self(void);
//返回调用线程的tid
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* function()
{
char* ptr = "返回值";
pthread_exit((void*)ptr);
}
void* func(void* arg)
{
pthread_detach(pthread_self());
while(1)
{
printf("thread\n");
sleep(5);
function();
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, func, NULL);
if(ret != 0)
return -1;
//char* ptr;
//pthread_join(tid, (void**)&ptr);
//printf("%s\n", ptr);
while(1)
{
printf("main\n");
sleep(1);
}
return 0;
}
4.线程安全
线程安全:多个执行流对临界资源进行争抢访问,但是不会出现数据二义性。
线程安全的实现
同步:通过条件判断保证对临界资源访问的合理性。
互斥:通过同一时间对临界资源访问的唯一性实现临界资源访问的安全性。
互斥的实现
互斥锁原理:互斥锁本身是一个只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源时都需要先判断当前的临界资源是否允许访问。不允许则让执行流等待,否则可以让执行流访问临界资源,在访问期间需要将状态改为不可访问状态,这期间如果其他执行流想要访问不被允许。
//1.定义互斥锁变量
pthread_mutex_t;
//2.初始化互斥锁变量
pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t* attr);//第一种
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;//第二种
//3.加锁
pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞加锁
//挂起等待:将线程状态置为可中断休眠状态 被唤醒:将线程状态置为运行状态
pthread_mutex_trylock(pthread_mutex_t* mutex);//如果不能加锁立即报错返回
//4.解锁
pthread_mutex_unlock(pthread_mutex_t* mutex);
//5.销毁互斥锁
pthread_mutex_destroy(pthread_mutex_t* mutex);
int ticket = 100;
pthread_mutex_t mutex;
void* func(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
usleep(1000);
printf("我抢到票了--%d\n", 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, func, NULL);
if(ret != 0)
return -1;
}
for(i = 0;i < 4;i++)
{
pthread_join(tid[i], NULL);
}
//互斥锁的销毁一定是不再使用这个互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
互斥锁本身就是一个临界资源,大家都会访问,所以需要保证互斥锁本身的操作是安全的。
互斥锁本身是一个只有0/1的计数器,当一个线程访问临界资源进行加锁时,会将内存中互斥锁变量的值与cpu中寄存器的值(寄存器中的值为0)进行一步交换,不管当前互斥锁变量的状态是什么,进行一步交换之后,其它线程都是不可访问的,这时候当前线程就可以慢慢判断了。
死锁
死锁:多个执行流对锁资源进行争抢访问,但是因为访问推进顺序不当,造成互相等待最终导致程序流程无法继续推进。
死锁产生的必要条件:
1.互斥条件:我加了锁,别人就不能继续加锁。
2.不可剥夺条件:我加的锁,只有我能解。
3.请求与保持条件:我加了A锁,然后去请求B锁;如果不能对B加锁,则也不释放A锁。
4.环路等待条件:我加了A锁,然后去请求B锁;另一个加了B锁,然后去请求A锁。
死锁的预防:破坏死锁产生的必要条件(主要是3/4两个条件)
死锁的避免:死锁检测算法/银行家算法
银行家算法的思路:系统的安全状态/非安全状态
一张表记录当前有那些锁,一张表记录已经给谁分配了哪些锁,一张表记录谁当前需要那些锁。按照三张表进行判断,判断若给一个执行流分配了指定的锁,是否会达成环路等待条件导致系统的运行进入不安全状态,如果有可能就不能分配。反之,若分配了之后不会造成环路等待,系统是安全的,则分配这个锁(破坏环路等待条件)。
后续若不能分配锁,可以资源回溯,把当前执行流中已经加的锁释放掉(破坏请求与保持条件)。
非阻塞加锁操作,若不能加锁,则把手上的其他所也释放掉(破坏请求与保持条件)。
同步的实现
同步的实现:通过条件判断实现临界资源访问的合理性—条件变量。判断当前是否满足获取资源的条件,若不满足,则让执行流等待,等待满足条件能够获取的时候再唤醒执行流。
//1.定义条件变量
pthread_cond_t;
//2.初始化条件变量
pthread_cond_init(pthread_cond_t*, pthread_condattr_t*);//第一种
pthread_cond_t cond = PTHREAD_COND_INITALIZER;//第二种
//3.使线程挂起休眠的接口
pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*);
//一直等待唤醒
pthread_cond_timedwait(pthread_cond_t*, pthread_mutex_t*, struct timespec);
//等待指定时间内都没有被唤醒则自动醒来
//4.唤醒线程
phtread_cond_signal(pthread_cond_t*);
//唤醒至少一个等待的线程
pthread_cond_broadcast(pthread_cond_t*);
//唤醒所以等待的线程
int bowl = 0;
pthread_cond_t customer;
pthread_cond_t cook;
pthread_mutex_t mutex;
void* thr_cook(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(bowl != 0)
{
pthread_cond_wait(&cook, &mutex);
}
bowl = 1;
printf("我做了一碗饭\n");
pthread_cond_signal(&customer);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_customer(void* arg)
{
while(1)
{
while(bowl != 1)
{
pthread_cond_wait(&customer, &mutex);
}
bowl = 0;
printf("我吃了一碗饭\n");
pthread_cond_signal(&cook);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t cook_tid[4], customer_tid[4];
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cook, NULL);
pthread_cond_init(&customer, NULL);
int ret;
int i;
for(i = 0;i < 4;i++)
{
ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
if(ret != 0)
return -1;
ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
if(ret != 0)
return -1;
}
pthread_join(cook_tid[0], NULL);
pthread_join(customer_tid[0], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cook);
pthread_cond_destroy(&customer);
return 0;
}
注意事项:
1.条件变量使用中对条件的判断应该使用while循环。
2.多种角色线程应该使用多个条件变量。