[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

线程演示代码:

cpp 复制代码
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;

条件变量是在加锁内使用的。

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

相关推荐
心灵彼岸-诗和远方19 分钟前
高效协作:前后端合作规范与应对策略优化
java·架构·devops
我来变强了20 分钟前
EndpointConfig端点配置类使用
java·websocket·网络协议
Horacek23 分钟前
《C++ 实现生成多个弹窗程序》
java·开发语言·c++·学习·算法
白羊@25 分钟前
01、Spring MVC入门程序
java·spring·mvc
曾经的三心草28 分钟前
JavaWeb之AJAX
java·ajax·json·axios·web
zero_one_Machel35 分钟前
leetcodeQ76最小覆盖子串
java·开发语言·算法
Learning改变世界37 分钟前
DNS服务器Mac地址绑定与ip网路管理命令(Ubuntu24.04)
linux·服务器·网络
长安 故里1 小时前
TiDB v7.5.4安装部署
java·linux·tidb
运维佬1 小时前
要卸载 Grafana 或者从 TiDB 集群中删除 Grafana 服务节点,你需要按以下步骤操作
java·tidb·grafana
运维佬1 小时前
在Linux环境下部署TiDB可以通过几种不同的方法
linux·docker·tidb