加入收藏 | 设为首页 | 会员中心 | 我要投稿 拼字网 - 核心网 (https://www.hexinwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 服务器 > 搭建环境 > Linux > 正文

1.2 线程与进程的关系

发布时间:2023-01-05 10:03:57 所属栏目:Linux 来源:网络
导读: 文章目录
1. 概念 1.1 为什么要用线程
由于进程的地址空间是私有的,多个进程在运行的时候是上下文切换执行的,如果要从一个进程切换到另一个进程执行,那么就需要将之前进程的用户空间切换

文章目录

1. 概念 1.1 为什么要用线程

由于进程的地址空间是私有的,多个进程在运行的时候是上下文切换执行的,如果要从一个进程切换到另一个进程执行,那么就需要将之前进程的用户空间切换到新的进程,由于用户空间有3G,所以切换起来是比较消耗资源,系统开销比较大

为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程,多个线程共用同一个进程的资源,所以线程间来回切换时不用消耗太多资源,所以如果一个程序要执行多个不同任务,会选择使用多线程来实现

在同一个进程中创建的线程共享该进程的地址空间

Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度

1.2 线程与进程的关系 在操作系统设计上,从进程演化出线程,主要为了更好地支持多处理器,减少进程上下文切换的开销线程属于进程,线程运行在进程空间内,同一进程所产生的线程共享同一用户内存空间,当进程退出时,进程所产生的线程都会被强制退出并清除。一个进程至少需要一个线程作为它的指令执行体进程管理着资源(cpu、内存、文件等),将线程分配到某个cpu执行。进程是系统资源管理的最小单位,线程是程序执行的最小单位每个进程都有自己的数据段、代码段和堆栈段。线程是轻型的进程,有独立的栈和CPU寄存器状态,线程是进程的一条执行路径,每个线程共享其所附属进程的所有资源,包括打开的文件、内存页面、信号标识以及动态分配的内存等线程和进程比起来较小线程池linux, 线程会花费更少的CPU资源

在这里插入图片描述

1.3 主控线程

默认情况下,一个进程中只有一个线程,称为主控线程

由主控线程创建的线程称之为子线程,不管是主控线程还是子线程,都附属于当期的进程

1.4 编译 含有线程的程序

多线程通过第三方线程库实现

线程库 libpthread.so

手动链接库文件 gcc xxx.c -lpthread

2. 线程相关函数 2.1 pthread_create() 创建一个子线程

#include 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
           void *(*start_routine) (void *), void *arg);
功能:
	创建一个子线程
参数:
    thread:创建的线程的线程号
    attr:线程的属性,一般设置为NULL表示默认属性
    start_routine:线程处理函数
    arg:给线程处理函数传参的
返回值:
    成功:0
    失败:非0

2.1.1 线程间调度机制

线程的调度机制 时间片轮转,上下文切换

注意:虽然线程的处理代码就是一个函数,也满足函数的性质,但是这个函数的执行顺序与一般的普通函数完全不同

子线程的执行代码只是在对应的线程处理函数里面,函数执行结束,当前线程也就结束了,如果线程所在的进程在某一时刻结束,那么整个进程中所有的线程都会强制清除

2.1.2 线程多资源使用问题

多线程共享同一个进程的资源,例如全局变量,这个进程中的线程都可以去操作,但是需要考虑资源抢占的问题

每一个线程的执行就是一个函数,所以线程的资源与普通函数几乎没有区别,控线程的数据可以通过pthread_create的最后一个参数传递给线程处理函数

2.2 pthread_self() 获取自己的线程号

#include 
pthread_t pthread_self(void);
功能:
	获取调用者的线程号
参数:
	无
返回值:
    成功:本线程的线程号  
    
    本函数总是执行成功。

2.3 pthread_exit() 退出本线程

#include 
void pthread_exit(void *retval);
功能:
	在指定线程中调用,用于退出一个线程
参数:
    retval:线程退出时返回的值,可以被其他同进程的线程调用pthread_join接收
返回值:
	无

注意:主控线程结束,进程是没有结束的,所以子线程还是可以继续运行的,但是进程如果结束,当前进程中所有的线程都会立即结束

2.4 pthread_cancel() 向指定线程发送结束请求

#include 
int pthread_cancel(pthread_t thread);
功能:
	向指定的线程发送结束请求
参数:
    thread:接收请求的线程,默认接收到请求线程就会结束
返回值:
    成功:0
    失败:非0

2.5 pthread_join() 阻塞等待线程结束

#include 
int pthread_join(pthread_t thread, void **retval);
功能:
	阻塞等待线程的结束
参数:
    thread:指定要等待的线程的id
    retval:线程结束后的状态值,主要接收pthread_exit所设置的值
返回值:
    成功:0
    失败:非0

2.6 pthread_detach() 将线程设置为分离态

线程的分离态和结合态

#include 
int pthread_detach(pthread_t thread);
功能:
	将线程设置为分离态
参数:
    thread:线程id
返回值:
    成功:0
    失败:非0

如果将线程设置为了分离态,pthread_join就无法使用了

3. 线程的同步与互斥机制 3.1 概念

互斥:同一时间只能有一个线程执行,执行完毕后其他线程再执行

线程中的互斥机制 → 斥锁

同步:在互斥的基础上有顺序执行

线程中的同步机制 → 信号量 条件变量

学习线程间同步与互斥机制就是要解决多个线程对共一个共享资源操作时可能会产生的问题,将多线程对同一个资源操作抢占问题称之为竞态

3.2 互斥锁 运行机制

多个线程如果要对同一个共享资源进行操作,谁抢占到资源,就对共享资源的操作

进行上锁,执行操作,其他线程需要等待该线程解锁后,才能继续争抢对资源操作的权限。

这个锁或者操作权限就是互斥锁。

互斥锁,锁的是对资源的操作权限。

操作之前上锁,操作完毕后解锁,解锁之后其他线程又可以操作,以此类推

3.2.1 pthread_mutex_init() 初始化一个互斥锁

    #include 
    int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);
    功能:
    	初始化一个互斥锁
    参数:
        mutex:互斥锁
        mutexattr:互斥锁的属性,设置为NULL表示默认属性
    返回值:
        成功:0

3.2.2 pthread_mutex_lock 上锁

    #include 
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    功能:
    	上锁
    参数:
        mutex:互斥锁
    返回值:
        成功:0
        失败:非0

3.2.3 pthread_mutex_unlock 解锁

    #include 
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    功能:
    	解锁
    参数:
        mutex:互斥锁
    返回值:
        成功:0
        失败:非0

3.2.4 pthread_mutex_destory 销毁互斥锁

    #include 
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    功能:
    	销毁一个互斥锁
    参数:
        mutex:互斥锁
    返回值:
        成功:0
        失败:非0

3.2.5 互斥锁 示例

#include 
#include 
#include 
#include 
//互斥锁的操作
//注意:互斥锁上锁解锁都是对共享资源的操作来执行,不是对共享资源
//第一步:创建一个互斥锁
pthread_mutex_t mutex;
int money = 10000;
void *zhangsan_fun(void *arg)
{
    int yu, qu = 10000;
    int shiji;
    //第三步:对共享资源的操作上锁
    pthread_mutex_lock(&mutex);
    printf("张三正在查询余额...\n");
    sleep(1);
    yu = money;
    printf("张三正在取钱...\n");
    sleep(1);
    if(yu < qu)
    {
        shiji = 0;
    }
    else 
    {
        yu = yu - qu;
        money = yu;
        shiji = qu;
    }
    printf("张三想取%d,实际取了%d,余额为%d\n", qu, shiji, yu);
    //第四步:共享资源操作完毕后解锁
    pthread_mutex_unlock(&mutex);
}
void *lisi_fun(void *arg)
{
    int yu, qu = 10000;
    int shiji;
    //第三步:对共享资源的操作上锁
    pthread_mutex_lock(&mutex);
    printf("李四正在查询余额...\n");
    sleep(1);
    yu = money;
    printf("李四正在取钱...\n");
    sleep(1);
    if(yu < qu)
    {
        shiji = 0;
    }
    else 
    {
        yu = yu - qu;
        money = yu;
        shiji = qu;
    }
    printf("李四想取%d,实际取了%d,余额为%d\n", qu, shiji, yu);
    //第四步:共享资源操作完毕后解锁
    pthread_mutex_unlock(&mutex);
}
int main(int argc, char const *argv[])
{
    //第二步:初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    pthread_t zhangsan, lisi;
    if(pthread_create(&zhangsan, NULL, zhangsan_fun, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    if(pthread_create(&lisi, NULL, lisi_fun, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    pthread_join(zhangsan, NULL);
    pthread_join(lisi, NULL);
    //第五步:使用完毕销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

3.3 同步之信号量 运行机制

信号量主要解决同步问题

信号量的本质是P V操作

信号量的本质是一个计数器,对计数器可以进行P操作和V操作,P操作是减操作,V操作时加操作

当信号量为0时,P操作会阻塞,V操作不影响

默认线程中的信号量PV操作执行一次只能减1或者加1

3.3.1 sem_init() 初始化信号量

    #include 
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    功能:
    	初始化一个信号量
    参数:
        sem:信号量
        pshared:判断是在线程间使用还是进程间使用
            0 线程间使用
            1 进程间使用
       value:信号量的初始值
    返回值:
        成功:0
        失败:-1

3.3.2 sem_post() 执行V操作

    #include 
    int sem_post(sem_t *sem);
    功能:
    	执行V操作,执行一次信号量的值加1
    参数:
        sem:信号量
    返回值:
        成功:0
        失败:-1

3.3.3 sem_wait() 执行P操作

    #include 
    int sem_wait(sem_t *sem);
    功能:
    	执行P操作,执行一次信号量的值减一,
    	当信号量的值为0时,无法执行P操作,会阻塞
    参数:
        sem:信号量
    返回值:
        成功:0
        失败:-1

3.3.4 sem_destory() 销毁一个信号量

    #include 
    int sem_destroy(sem_t *sem);
    功能:
    	销毁一个信号量
    参数:
        sem:信号量
    返回值:
        成功:0
        失败:-1

3.3.5 信号量 示例

生产者消费者问题 (做蛋糕买蛋糕)

在这里插入图片描述

#include 
#include 
#include 
#include 
#include 
//两个线程循环先后执行,需要创建两个信号量来控制
//创建一个信号量
sem_t sem1, sem2;
void *thread_fun1(void *arg)
{
    while(1)
    {
        sem_wait(&sem2);
        printf("正在做蛋糕...\n");
        sleep(1);
        printf("蛋糕做好了...\n");
        
        //第四步:先执行的线程执行完毕后执行V操作
        sem_post(&sem1);
    }
    
}
void *thread_fun2(void *arg)
{
    while(1)
    {
        //第三步:后执行的线程执行P操作
        sem_wait(&sem1);
        
        printf("正在买蛋糕...\n");
        sleep(1);
        printf("蛋糕买完了...\n");
        sem_post(&sem2);
    }
}
int main(int argc, char const *argv[])
{
    //第二步:初始化一个信号量
    sem_init(&sem1, 0, 0);
    sem_init(&sem2, 0, 1);
    pthread_t thread1, thread2;
    if(pthread_create(&thread1, NULL, thread_fun1, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    if(pthread_create(&thread2, NULL, thread_fun2, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    //第五步:销毁信号了
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    while(1)
    ;
    return 0;
}

扩展:

编写一个程序,开启三个线程,这三个线程分别打印A、B、C,每个线程打印10遍,要求输出必须按照ABC的顺序显示,如:ABCABCABC…

在这里插入图片描述

3.4 线程同步之条件变量 运行机制

条件变量常用于解决同步问题

条件变量主要有两个操作,发送信号通知和等待信号通知。

如果没有线程发送信号通知,那么等待信号通知的线程就会一直阻塞,直到有线程发送信号通知,就会唤醒这个线程。

3.4.1 pthread_cond_init() 初始化条件变量

    #include 
    int  pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t*cond_attr);
    功能:
    	初始化一个条件变量
    参数:
        cond:条件变量
        attr:属性,设置为NULL表示默认属性
    返回值:
         成功:0
        失败:非0

3.4.2 pthread_cond_signal() / pthread_cond_booadcast() 给线程发送信号通知

    #include 
    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond);
    功能:
        pthread_cond_signal 只给一个线程发送信号通知
        pthread_cond_broadcast 是给所有线程发送信号通知
    参数:
        cond:条件变量
    返回值:
        成功:0
        失败:非0

3.4.3 pthread_cond_wait() 等待信号通知

    #include 
    int  pthread_cond_wait(pthread_cond_t  *cond, pthread_mutex_t *mutex);
    功能:
    	等待信号通知
    参数:
        cond:条件变量
        mutex:互斥锁
    返回值:
        成功:0
        失败:非0

3.4.4 pthread_cond_destory() 销毁条件变量

    #include 
    int pthread_cond_destroy(pthread_cond_t *cond);
    功能:
    	销毁一个条件变量
    参数:
        cond:条件变量
    返回值:
        成功:0
        失败:非0  

3.4.5 条件变量 示例

#include 
#include 
#include 
#include 
int num = 0;
pthread_mutex_t mutex;
//创建一个条件变量
pthread_cond_t cond;
//注意:当使用条件变量解决同步问题时,一定要保证后执行的线程先执行
//pthread_cond_wait函数,保证先让后执行的线程先等待信号通知,然后
//先执行的线程执行完毕后在发送信号通知
void *thread_fun1(void *arg)
{
    printf("正在做蛋糕...\n");
    sleep(1);
    printf("蛋糕做好了...\n");
    pthread_mutex_lock(&mutex);
    num = 1;
    //先执行的线程执行完毕后发送信号通知
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
}
void *thread_fun2(void *arg)
{
    //后执行的线程先执行接收信号通知函数
    //一般在执行pthread_cond_wait函数之前需要先上锁
    pthread_mutex_lock(&mutex);
    //pthread_cond_wait函数内部逻辑:
    //第一步:解锁
    //第二步:上锁
    //第三步:将要等待的线程添加到等待队列中
    //第四步:解锁
    //第五步:上锁
    pthread_cond_wait(&cond, &mutex);
    if(num > 0)
    {
        printf("正在买蛋糕...\n");
        sleep(1);
        printf("蛋糕买完了...\n");
        num--;
    }
    pthread_mutex_unlock(&mutex);
}
int main(int argc, char const *argv[])
{
    pthread_mutex_init(&mutex, NULL);
    //初始化一个条件变量
    pthread_cond_init(&cond, NULL);
    pthread_t thread1, thread2;
    if(pthread_create(&thread1, NULL, thread_fun1, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    if(pthread_create(&thread2, NULL, thread_fun2, NULL) != 0)
    {
        perror("pthread_create error");
        exit(1);
    }
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_mutex_destroy(&mutex);
    //销毁条件变量
    pthread_cond_destroy(&cond);
    while(1)
    ;
    return 0;
}

4. 死锁 4.1 死锁概念

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

4.2 产生死锁的原因 系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争,才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞

4.3 如何避免死锁 加锁顺序

(线程按照一定的顺序加锁)

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

加锁时限

(线程尝试获取锁的时候,限定时间,如果超时则放弃对该锁的请求,并释放自己占有的锁)

在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

_

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

_

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

_

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

(编辑:拼字网 - 核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!