线程概念:
线程概念:线程是进程中的一条执行流,负责进程中一段代码的运行调度,一个进程中可以有一个或多个线程。
在Linux下,线程是通过pcb实现的,一个进程中可以有多个pcb,这些pcb共享进程中的大部分资源,因此也被称作轻量级进程。
线程就是进程中的一条执行流,而在Linux下执行流通过pcb来实现,一个进程中可以有多个执行流pcb,这些pcb共享进程中的大部分资源,相较于传统pcb较为轻量化,因此也被称作轻量级进程。
线程与进程的关键性区别:
进程是系统进行资源分配的基本单元
线程是系统进行运行调度的基本单元
线程间的独有与共享:
独有:标识符、栈、寄存器、信号屏蔽字、errno…
共享:虚拟地址空间、IO信息、信号处理方式、工作路径…
多进程与多线程进行多任务处理的优缺点:
线程优点:
1、线程间通信更加灵活,包含进程间通信在内,还可以使用全局变量以及函数传参实现通信
2、线程的创建与销毁成本更低
3、线程间的调度成本低
进程的优点:稳定,健壮。 应用场景:shell、网络服务器…
线程不是越多越好,cpu资源是固定的,线程给多了反而会增加线程的切换调度成本
cpu密集型程序:程序中进行大量的数据运算,对cpu资源要求高
IO密集型程序:程序中进行大量的IO操作,对cpu资源要求并不高
线程控制
线程控制包括四个方面:创建、退出、等待、分离
创建
操作系统在底层并不会区分轻量级进程与线程,所以操作系统实际上并没有提供用于创建线程的接口,后来的大牛们通过封装实现了线程控制对应所需的库函数,用于对线程操作。
int pthread_creat(pthread_t *tid, pthread_attr_t *attr,
void*(*routine)(void*), void *arg);
pthread_t *tid:(输出型参数) 用于接收线程ID----tid
pthread_attr_t *attr: 设置线程属性----通常置NULL
void*(*routine)(void*): 线程入口函数,入口函数运行完毕后,退出线程
void *arg:传递给线程的数据,就是传递给上面这个入口函数的形参
返回值:
成功返回0;失败返回非0—错误编号。
代码示例:
注意:线程的操作都是通过库函数来实现的,所以在编译和运行相关代码时,需要链接pthread动态库
命令操作:gcc creat.c -o creat -lpthread
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void *thr_entry(void* arg){
while(1){
printf("normal thread--%s\n",(void*)arg);
sleep(1);
}
return NULL;
}
int main(){
//pthread_create(tid, 线程属性, 入口函数, 参数 )
pthread_t tid;
char *ptr="好好学习~";
int ret=pthread_create(&tid,NULL,thr_entry,(void*)ptr);
if(ret!=0){
printf("thread_create error!");
return -1;
}
while(1){
printf("天气真好\n");
sleep(1);
}
return 0;
}
退出
1、在线程入口函数中return;线程入口函数运行完毕,线程就会被销毁
注意:main函数中return会直接销毁进程,退出所有的线程
2、调用函数接口退出:
void pthread_exit(void* retval); //退出调用该接口的线程
int pthread_cancel(pthread_t id); //取消指定的线程--非正常退出
注意:在主线程中调用pthread_exit()
接口并不会导致所有线程退出,而是仅仅退出主线程。
等待
概述:等待指定的线程退出,获取退出线程的返回值,回收资源
一个处于joinable状态
(线程的分离属性默认是joinable状态)的线程在退出时,操作系统不会自动释放其资源,所以需要线程等待。
int pthread_join(pthread_t tid,void** retval);
tid:指定要等待的线程
tval:用于获取线程返回值
分离
将线程的分离属性设置为detach状态;
处于detach状态的线程退出时会自动释放资源,不需要被等待。
int pthread_detach(pthread_t tid);
线程安全
表示线程中对临界资源的访问是安全的
临界资源:公共资源,每个线程都能访问到的资源
线程安全的实现:同步与互斥
互斥:同一时间只能有一个线程访问资源,保证资源访问的安全性;
同步:通过条件判断,让线程对临界资源的访问更加合理有序
互斥:互斥锁
互斥锁
互斥锁:通过互斥锁保护线程对临界资源的访问不被打断
本质:只有0或1两种状态的计数器
原理:标记临界资源的两种状态,可访问和不可访问;在线程访问资源之前先进行加锁
操作(判断是否可访问,可访问则返回;不可访问则阻塞),加锁操作之后会将资源置为不可访问状态。
当线程访问完毕后进行解锁
操作,将资源置为可访问状态。
操作步骤:
1、定义一个互斥锁变量:
pthread_mutex_t mutex;
2、初始化互斥锁:①、定义互斥锁时进行初始化:
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
②、接口操作:int pthread_mutex_init (pthread_mutx_t * restrict mutex, const pthread _mutexattr_t * restrict attr);
3、在访问临界资源之前进行加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
–阻塞操作
int pthread_mutex_trylock(pthread_mutex_t *mutex);
–非阻塞操作
4、在访问临界资源完毕之后解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
5、不在使用互斥锁则销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
代码示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int tickets=100;
void* scalpers(void* arg){
pthread_mutex_t* mutex=(pthread_mutex_t*)arg;
pthread_t tid=pthread_self();
while(1){
pthread_mutex_lock(mutex); //加锁
if(tickets>0){
usleep(1);
printf("I got a ticket!-%p-%d\n",tid,tickets);
tickets--;
pthread_mutex_unlock(mutex); //解锁
}else{
pthread_mutex_unlock(mutex); //在任意有可能造成线程退出的地方都要进行解锁
pthread_exit(NULL);
}
}
return NULL;
}
int main(){
pthread_mutex_t mutex;
pthread_t tid[4]={0};
int ret=0;
pthread_mutex_init(&mutex,NULL); //互斥锁的初始化必须在线程创建之前
for(int i=0;i<4;i++)
{
ret=pthread_create(&tid[i],NULL,scalpers,&mutex);
if(ret!=0){
printf("pthread_create error\n");
return -1;
}
}
for(int i=0;i<4;i++){
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
死锁
描述的是程序因为流程无法继续推进而卡死的情况
死锁产生的原因:
1、互斥:同一时间只有一个线程能够访问资源
2、不可剥夺条件:一个线程加锁后,只能由自己来解锁,其他线程不能解
3、请求与保持条件:得到A锁后请求B锁,请求不到B锁,但是不释放A锁
4、环路等待条件:a线程拿到1锁,请求2锁;b线程拿到2锁,请求1锁
如何预防死锁:
1、保证加锁和解锁的顺序正确合理
2、破坏请求与保持条件:使用非阻塞加锁
如何解决死锁:银行家算法、死锁检测算法…
同步:条件变量
同步的实现:通过条件判断资源获取是否合理----条件变量
条件变量:一个pcb等待队列+使线程阻塞和唤醒的接口
原理:当资源获取不合理时,调用阻塞接口,使线程阻塞,将pcb挂到等待队列;等到资源访问合理时,通过唤醒接口唤醒阻塞的等待队列上的线程。
注意:条件变量本身只提供阻塞和唤醒功能,而资源获取是否合理需要程序员另外做判断
接口介绍:
1、定义条件变量:pthread_cond_t cond;
2、初始化条件变量:pthread_cond_t cond=PTHREAD_COND_INITALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
3、获取资源不合理时调用阻塞接口:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
4、其他线程促使资源获取合理后,唤醒等待队列上的线程:
int pthread_cond_signal(pthread_cond_t *cond);
----唤醒一个或者多个线程,至少一个
int pthread_cond_broadcast(pthread_cond_t *cond);
----唤醒所有
5、销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond)
实际应用:顾客厨师问题
编程解决以下问题:顾客来吃饭,首先判断柜台是否有饭,没饭就等待,有饭就吃饭;
厨师做饭,判断柜台是否有饭,有饭就等待,没饭就做饭。
注意:
1、条件的判断应该使用while循环判断,因为可能会有多名顾客同时来吃饭的情况
2、在具有多种角色的情况下,应该使用多个条件变量,否则会出现唤醒错误
代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
pthread_cond_t cond_customer;
pthread_cond_t cond_cooker;
pthread_mutex_t mutex;
int counter=0; //柜台 等于0表示柜台上没有饭
void *cooker(void* arg){
while(1){
//加锁
pthread_mutex_lock(&mutex);
while(counter==1){
//表示柜台上有饭,厨师不需要做饭,cooker阻塞
pthread_cond_wait(&cond_cooker,&mutex);
}
//做饭
printf("做了一碗饭!\n");
counter++;
//唤醒顾客
pthread_cond_signal(&cond_customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *customer(void* arg){
while(1){
//加锁
pthread_mutex_lock(&mutex);
while(counter==0){
//阻塞
pthread_cond_wait(&cond_customer,&mutex);
}
//吃饭
printf("吃完了,好吃!\n");
counter--;
//唤醒厨师
pthread_cond_signal(&cond_cooker);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_t tid1,tid2;
pthread_cond_init(&cond_customer,NULL); //初始化条件变量
pthread_cond_init(&cond_cooker,NULL); //初始化条件变量
pthread_mutex_init(&mutex,NULL); //初始化互斥锁
int ret=0;
for(int i=0;i<3;i++){
ret=pthread_create(&tid1,NULL,cooker,NULL); //创建cooker线程
if(ret!=0){
printf("thread create error!\n");
return -1;
}
}
for(int i=0;i<3;i++){
ret=pthread_create(&tid2,NULL,customer,NULL); //创建customer线程
if(ret!=0){
printf("thread create error!\n");
return -1;
}
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_cond_destroy(&cond_customer); //销毁条件变量
pthread_cond_destroy(&cond_cooker); //销毁条件变量
pthread_mutex_destroy(&mutex); //销毁互斥锁
return 0;
}
结果: