多线程控制&多线程安全&死锁&读写锁

多线程概念

在传统操作系统上pcb是一个进程,描述一个程序的运行,还有一个tcp描述实现线程,但是 在linux下使用pcb描述实现了程序调度并且这些pcb共用同一个虚拟地址空间,相较于传统的pcb更加轻量化一点,因此也把linux下的pcb称之为轻量级进程。

进程是系统资源分配的基本单位。
线程是CPU调度的基本单位。

线程间的独有与共享:

独有:栈,寄存器,信号屏蔽字,errno,标识符
共享:虚拟地址空间(代码段,数据段),文件描述表,信号处理方式,工作路径,用户ID,组ID

多进程/多线程进行多任务处理(优缺点):

多线程:线程间通信更加方便灵活(全局变量,函数传参)
线程的创建/销毁成本更低
线程间的调度成本更低
异常和某个系统调用针对的是整个进程。
多进程:具有独立性,因此更加稳定,健壮。
例如:对主功能程序安全性稳定性要求更高的最好使用多进程(shell/服务器),剩下的使用多线程
共同优点
CPU密集型程序/IO密集型程序
并行压缩CPU处理/并行压缩IO等待时间
在CPU资源足够的情况下,可以使程序的性能更高

线程控制

线程控制的接口都是库函数(操作系统并没有向用户提供创建一个轻量级进程的接口,因此大佬门才封装的一套线程控制接口,所以在使用的时候链接库文件 -lpthread)

线程创建

int pthread_create(pthread_t tid,pthread_attr_t attr,void(thread_routine)(voidarg),void arg);**

tid:用于获取线程id,通过这个id可以找到线程的描述信息,进而访问pcb(轻量级进程完成控制)【线程在虚拟地址空间中开辟的线程空间的首地址(线程信息)】
attr:线程属性,通常置NULL
thread_routine:线程入口函数,创建一个线程就是为了运行这个函数,函数运行完毕,则线程退出
arg:通过线程入口函数传递给线程的参数
返回值:0-成功,非0值-失败errno

ps -efL | grep create:查看线程
在这里插入图片描述
代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* thread_start(void* arg){

    while(1){
        printf("主线程传递了一个参数:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//无符号长整形
    char buf[]="好运来~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述

线程终止

1.普通线程入口函数中的return
注意:main函数中的return退出的是进程

2.void pthread_exit(viod retval);//退出一个线程,谁调用谁退出*

retval-线程返回值

代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* thread_start(void* arg){
   sleep(5);
   pthread_exit(NULL);
    while(1){
        printf("主线程传递了一个参数:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//无符号长整形
    char buf[]="好运来~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

3.int pthread_cancel(pthread_t tid);//退出指定的线程

tid:就是指定的线程id
代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* thread_start(void* arg){
    while(1){
        printf("主线程传递了一个参数:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//无符号长整形
    char buf[]="好运来~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    
    sleep(3);
    pthread_cancel(tid);
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

注意

1.线程退出不会完全释放资源,需要被其他线程等待。
2.取消自己是一种违规操作(主线程退出)。
3.主线程退出,并不影响整个进程的运行,只有所有的线程退出,进程才会退出。

线程等待

等待一个线程的退出,获取退出线程的返回值,回收这个线程所占用的资源。
int pthread_join(pthread_t tid,void** retval);

tid:指定要等待的线程id
retval:用于获取线程退出返回值

代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void* thread_start(void* arg){

    char* buf="我是锦鲤\n";
    sleep(5);
    pthread_exit(buf);
    while(1){
        printf("主线程传递了一个参数:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//无符号长整形
    char buf[]="好运来~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    void* retval=NULL;
    pthread_join(tid,&retval);
    printf("retval:%s\n",retval);
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述
注意:

不是所有的线程都能被等待,一个线程被创建默认情况下有一个属性-joinable;处于joinable属性的线程退出后,不会自动释放资源,需要被等待。

线程分离

将线程的属性从joinable设置为detach;处于detach属性的线程退出后会自动释放资源,不需要被等待。

等待一个被分离的线程,则pthread_join会返回错误:这不是一个joinable线程(因为在获取返回值的时候将获取不到,detach属性的线程退出后已经自动释放了资源)

int pthread_detach(pthread_t tid);//分离指定的线程
线程的分离可以在任意地方,可以在线程入口函数中让线程分离自己,也可以让创建线程在创建之后直接分离。

线程安全

在多个执行流中对同一个临界资源进行访问,而不会造成数据二义。

如何实现线程安全?

同步

通过一些条件判断来实现多个执行流对临界资源访问的合理性(有资源则访问,没有资源则等着,等有资源了再被唤醒)。
条件变量:

1.向用户提供两个接口:使一个线程等待的接口和唤醒一个线程的接口
2.等待队列
posix标准的信号量:

互斥

通过保证同一时间只有一个执行流可以对临界资源进行访问(一个执行流访问期间,其他执行流不能访问),来保证数据访问的安全性。
互斥锁:用于标记当前临界资源的访问状态

计数器:0/1 0-表示不可访问 1-表示可以访问
每一个线程访问临界资源之前,先判断计数,当前临界资源的状态(是否有人正在访问–正在访问的线程将状态置为了0(不可访问状态))
1.第一个线程访问的时候,判断可以访问,因此将状态置为不可访问,然后去访问资源
2.其他线程访问的时候,发现不可访问,就陷入等待(可中断休眠状态)
3.第一个线程访问临界资源完毕后,将状态置为1(可以访问)唤醒(将pcb状态置为运行态)等待线程,大家重新开始竞争这个资源

死锁

多个执行流在对多个锁资源进行争抢操作,但是因为推进顺序不当,而导致互相等待,流程无法继续推进的情况。
死锁产生的四个必要条件:

1.互斥条件:一个锁只有一个人能加,我加了锁,其他人就不能加了
2.不可剥夺条件:我加的锁,别人不能替我释放
3.请求与保持条件:我加了A锁,然后请求B锁,但是请求不到B锁,我也不释放A锁
4.环路等待条件:我拿着A锁请求B锁,对方拿着B锁请求A锁

预防死锁:破坏产生死锁的四个必要条件

1.锁资源按顺序一次性分配
2.加锁的时候可以使用非阻塞加锁,若无法加锁,则将手中的其他锁释放

避免死锁:

银行家算法:
定义两种状态:安全/非安全-
现在有多少资源
现在那些人已经借了多少钱
当前还有那些人需要借多少钱
若给一个执行流分配指定的锁有可能会造成环路等待(非安全状态),则不予分配,并且回溯释放当前执行流已有的资源

读者写者模型–读写锁

少量写临界资源的执行流+大量读临界资源的执行流
特性:不能同时写,但是可以同时读(写互斥,读共享),写的时候别人既不能写,也不能读;但是读的时候大家可以一起读,但是读的时候不能写

读写锁的实现:

两个计数器:读者计数/写者计数
读者计数:>0,表示当前有人正在读,想要加写锁的人就需要等待,而想要加读锁可以继续加
写者计数:>0,表示当前有人正在写, 想要加写锁的人就需要等待,想要加读锁也需要等待
其中不能加锁时的等待,通过自旋锁实现
自旋锁:循环判断条件,并且强占CPU(一直处于运行状态,判断条件,CPU不可剥夺),比较消耗CPU,比较适合于等待时间比较短的操作

发布了87 篇原创文章 · 获赞 73 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/HL_HLHL/article/details/103843438