自学内容网 自学内容网

[Linux]多线程详解

1.线程的概念和理解

在一个程序中的一个执行路线就是线程,更官方的定义就是线程是一个进程内部的控制序列

单线程中代码都是串行调用的。
我们想要实现并发调用也可以使用创建多进程的方法,但是创建进程是比较消耗资源的,要创建进程结构,页表,并建立映射关系,成本比较高;
但是线程就不一样了,在地址空间内创建的“进程”就叫做线程,只需要创建内核数据结构即可。

线程:就是进程内部的一个执行分支

1.1线程的优点

在这里插入图片描述

1.2线程的缺点

在这里插入图片描述

1.3线程的设计

线程也是要被管理的,进程有PCB,线程也有类似的东西(TCB)
在这里插入图片描述
但是这样创建非常的复杂,线程和进程有高度的相似性,没有必要单独设计这个算法,所以使用进程来模拟线程

1.4线程 VS 进程

在这里插入图片描述
从内核的角度出发,进程就是承担分配系统资源的基本实体。

那么多执行流是如何进行代码划分的?

在这里插入图片描述
一个32位分成了3个部分大小分别为10 10 12
前10个是页目录
中间10个是页表
在这里插入图片描述
页表如何找到相应的内存中的数据?
页表中的地址+ 虚拟地址后12位对应的数据(页内偏移)

给线程分配不同的区域,本质是就是给不同的线程,各自看到全部页表的子集

在这里插入图片描述

在这里插入图片描述

2.线程控制

linux中没有真线程,只有轻量级进程的概念,但是用户只认线程
所以linux中没有线程相关的系统调用,只有轻量级进程的系统调用,
pthread库—原生线程库——>将轻量级进程的系统调用进行封装,转换成线程相关的接口提供给用户。
所以我们在编写线程代码时必须 -pthread
在这里插入图片描述
线程演示代码:

void *threadrun(void *arg)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "新线程正在运行:" << cnt << "pid:" << getpid() << endl;
        sleep(1);
        cnt--;
    }
    return nullptr;
}
int main()
{
    // int pthread_create(pthread_t * thread, const pthread_attr_t *attr,
    // void *(*start_routine)(void *), void *arg);
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, nullptr);
    int cnt = 10;
    while (cnt)
    {
        cout << "主线程正在运行:" << cnt << "pid:" << getpid() << endl;
        sleep(1);
        cnt--;
    }
    return 0;
}

在这里插入图片描述
可以看出两个循环是一起运行的,所以进程中是有两个执行流的,并且他们是属于同一个进程。
主线程退出==进程退出,所有线程都要结束运行退出。

  1. 一般来说为了保证线程完成我们预期的工作,都是要保证主线程最后结束
  2. 线程也是需要被等待的,不然就会产生类似于进程退出的内存泄漏问题。
    补充:

ps -aL :会列出系统中所有具有终端的进程以及线程

2.1线程等待

在这里插入图片描述
void *retval:输出型参数,他就是线程的返回值(返回值为void)类型的

2.2 线程终止

同一个进程内,大部分资源都是共享的,其中就包括了地址空间。

线程出异常:
多线程中任何一个线程出现了异常,都会导致整个进程退出。

  1. 一个线程出问题,导致其他线程也出现问题,导致整个进程退出----线程安全问题
  2. 多线程中,公共函数被多个线程同时进入—出现重入问题

线程退出的时候不会像进程退出一样拿到退出信息,因为一但线程出异常之后,整个进程都会退出,没有时间来进行线程等待。
注意:线程退出不可以使用exit,这样会导致整个进程退出。

线程退出的三种方式

  1. return
  2. pthread_exit在这里插入图片描述
  3. pthread_cancel:取消线程(让主线程去取消目标线程)在这里插入图片描述

2.3 线程分离

默认情况下线程也是要被等待的,但是线程也是可以手动设置分离的。
如果主线程不关系新线程的执行结果,我们可以把新线程设置为分离状态。
在这里插入图片描述
被分离之后的线程就不可以再join了,不然就会出错。

线程分离:底层依旧是一个进程,一般都希望主线程最后一个退出,所以在线程分离中,主线程一般都是永远不退出的。

3.线程互斥

3.1背景

多个执行流共享的资源是共享资源,但是我们把他保护起来,一次只允许一个线程访问,这个就叫做临界资源
在代码中,访问临界资源的代码就叫做临界区

互斥:任何时刻,只允许一个执行流进入临界区,访问临界资源。
原子性:不会被任何调度机制大端,只有两种形态,要么完成,要么未完成。

3.2抢票代码演示

在这里插入图片描述
在这里插入图片描述
tickets>0;这是一个逻辑运算,当处理时,cpu会把内存中的数据拷贝到寄存器中,但是当进行到usleep时,这个线程就会进入等待队列,并且带走自己的上下文数据,没有执行到tickets–的位置,就会导致判断失误,会有很多的线程进入到这个抢票逻辑中取。

当进入到–操作时,把数据从内存读取到cpu,cpu进行内部–,再重新写回内存。

我们再从编译的角度去理解原子性
如果一个代码转换成汇编只有一条语句那他就是原子的
例如 - -操作,就不是原子,他会被转化为3条语句
在这里插入图片描述

3.3保护公共资源(加锁)

3.3.1创建锁/销毁锁

在这里插入图片描述
如果定义的锁是静态的或者是全局的,就不需要初始化也不需要销毁
直接:

pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER;

3.3.2申请锁/尝试申请锁/解锁

在这里插入图片描述申请锁/尝试申请锁,区别就是申请锁出错会阻塞,而另一个出错直接返回。

3.4解决抢票的出错逻辑

出现并发访问的问题,本质就是因为多个执行流并发访问全局数据的代码导致的,保护共享资源本质就是保护临界区
在这里插入图片描述
加锁的本质就是把并发访问的代码,变成串行访问,并且加锁的粒度要越细越好。

在这里插入图片描述
在这里插入图片描述
注意:有些平台会出现上面的问题,加锁之后,不同线程对锁的竞争强度不同,这算是一个bug,原则上竞争锁是自由的,竞争锁的能力太强就会导致饥饿问题。

3.5 理解锁

在这里插入图片描述

4.线程同步(条件变量)

一个线程跑完就接着下一个,解决饥饿问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

条件变量是在加锁内使用的。
从条件变量的函数来看,其实他和锁的用法极其的相似。


原文地址:https://blog.csdn.net/m0_73802195/article/details/143749676

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!