Linux 多线程

文章目录

一、背景知识

1、地址空间

[!IMPORTANT]

  • os进行内存管理,不是以字节为单位,而是以内存块为单位。(内存块默认大小4KB)
  • 系统和磁盘进行IO的基本单位是4KB(8个连续的扇区)

2、物理内存

image-20240822180637960

3、页表

image-20240822180800282

4、文件缓冲区、虚拟地址

什么是文件缓冲区?

将物理内存的页框(struct page)和文件(struct file)关联起来,这个页框就是文件缓冲区。

什么是函数?

连续的代码地址构成的代码块。

函数有地址吗?

有,每行代码都有地址,而且对于同一个函数内部的代码语句,我们认为地址是连续的。

代码数据划分的本质是什么?

拆分页表。

虚拟地址的本质是什么?

虚拟地址本质是一种资源!拥有者可以通过页表进行访问。

二、多线程

1、线程的概念

[!IMPORTANT]

  • 线程定义:在进程内部运行,是cpu调度的基本单位
  • 进程定义(内核观点):承担分配系统资源的基本实体

可以粗略的理解为:同一个进程内,一个task_struct对应的执行流就是一个线程。

image-20240822181548751

os怎么管理线程?

[!IMPORTANT]

先描述、再组织。

描述:struct tcb{//线程id,优先级,状态,上下文,连接属性…};

但是,在Linux中,线程是复用的进程的pcb。这样就不需要单独为线程单独设计数据结构和调度算法了。

Linux中,进程和线程的关系?

[!IMPORTANT]

在Linux中,没有线程的概念,只有轻量级进程的概念。

一个进程中,可能存在不止一个执行流,也就是说,可能存在多个pcb。以前学的进程是一个进程仅存在一个pcb,而现在是一对多的关系。

Linux中没有线程的概念,用户是怎么用线程的?

[!IMPORTANT]

在Linux底层中没有线程的概念,只有轻量级进程,为了上层能用线程,Linux把它进行了封装成了线程库。所以要用到线程的时候,需要连接一个线程库。

image-20240822182745578

2、线程的操作

创建线程

想要创建线程,必须手动连接pthread库

g++ $^ -o &@ -lpthread
// $^:是目标文件 --- main.exe->可执行程序
// $@:是依赖文件 --- main.cc->源文件
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 第三个参数,传一个函数指针,新线程所要执行的代码
// 第四个参数,作为第三个函数指针指向函数的参数。

// 主线程继续向下运行,新线程去执行其他的代码。
// 想要创建线程,必须手动连接pthread库
image-20240822211908525线程的唯一标识

对于同一个进程中的不同线程,两个线程的PID相同,但是LWP不同。

所以,唯一标识一个线程的是LWP(Light Weight Process:轻量级进程)

image-20240822202912643

LWP作为线程唯一标识,对单线程有影响吗?

没有影响,因为对于单进程而言,LWP 和 PID是一一对应的。

已经有多进程了,为什么还需要多线程?

[!IMPORTANT]

  • 进程的创建,成本高;线程的创建,成本低。—启动
  • 线程的调度成本低。—运行
  • 删除一个线程的成本低。—删除

线程的优势:

image-20240822205718215

线程的缺点:

对于一个进程中的多个线程,只要有一个线程出现了问题,多个线程都会崩溃。

对于不同系统实现线程的方式

[!IMPORTANT]

  • 不同的系统对于进程和线程的实现方式都不一样,但是实现的原则都是一样的。
  • 对于Linux:没有单独实现线程,只是复用pcb。
  • 对于Windows:单独实现线程。
为什么线程调度的成本更低?

[!IMPORTANT]

cpu的寄存器只有一套。当调度另外一个线程时,cpu中,cache等寄存器的上下文数据和物理空间之间的拷贝的操作。

如果是两个进程的话,代码可能不一样,对与进程a来说cache里面存储的代码,在进程b用不上,需要重新进行加载。

不同线程之间的共享部分?

[!IMPORTANT]
大部分地址空间资源,多线程都是共享的!

不同线程之间的私有部分?

[!IMPORTANT]

  • 一组寄存器:硬件上下文数据—线程可以动态运行。
  • 栈:每个线程都要有自己的栈结构。线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈空间。
全面看待线程函数传参?

[!IMPORTANT]

参数是能传递进去的,只需要在线程函数中将参数强转成需要的类型。

我们可以传递任意类型,可以传递类对象地址(用于传递多个参数)。

传递线程函数参数是类对象的时候,建议从堆上空间开辟。

全面的看待线程函数返回?

[!IMPORTANT]

线程退出的时候,只需要考虑正常的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程。

创建多线程
std::vector<pthread_t> tids;
for (int i = 0; i < num; i ++ )
{
   
    pthread_t tid;
    char *name = new char[128];
    snprintf(name, 128, "thread-%d", i+1);
    pthread_create(&tid, nullptr, threadrun, name);
    
    tids.emplace_back(tid);
}

for(auto tid:tids)
{
   
    pthread_join(tid, nullptr);
}

线程等待

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

image-20240822212703838

主线程和新线程哪一个先运行?

[!IMPORTANT]

不确定!

我们期望谁最后退出?怎么能保证呢?

[!IMPORTANT]

主线程应该最后退出。理由是要和子进程一样,退出信息要被父进程接收。

保证主线程最后退出:用pthread_join函数来进行保证,如果主线程先完成部分代码,会等待新线程。

tid是什么呢?(pthread_t tid)

[!IMPORTANT]

是一个地址(虚拟地址)。

给用户提供的的线程ID,不是内核中的lwp,而是pthread库维护的一个唯一值。

线程退出

[!IMPORTANT]

线程退出有三种情况:1、正常退出(线程函数返回 或者 主线程返回)2、通过pthread_exit退出 3、线程被取消(pthread_cancel)

#include <pthread.h>
void pthread_exit(void *retval); // 线程退出

#include <pthread.h>
int pthread_cancel(pthread_t thread); // 线程取消
// pthread_cancel是在主线程内部调用函数。

#include <pthread.h>
int pthread_detach(pthread_t thread); // 线程分离
// 可以在线程函数中调用线程分离函数,也可以在主线程中调用线程分离函数。

线程退出

线程如何终止?

[!IMPORTANT]

1、新线程:线程函数return。

2、主线程:main函数结束,表示进程结束。

可不可以不join线程,让他执行完就退出呢?

[!IMPORTANT]

可以!

a、一个线程被创建,默认是joinable的,必须要被join的。

b、如果一个线程被分离,线程的工作状态->分离状态,不需要被join,也不能被join。依旧属于线程内部,但是不需要被等待。

库的理解

[!IMPORTANT]

创建线程,前提就是把库加载到内存,映射到进程的地址空间!

  • 每个线程都有自己独立的栈,本质上就是线程在自己的tcb中维护了一段大小合适的栈结构。
  • Linux线程 = pthread中线程的属性集+LWP

image-20240823001814169

image-20240823001845969

在新线程内部获取该线程的线程id
#include <pthread.h>
pthread_t pthread_self(void);
// 在线程的内部,获取线程id -> 也就是tid
怎么保证pthread中线程和LWP一一对应?

os要提供一个LWP相关的系统调用。pthread库就是对其进行封装。

线程互斥(互斥锁)

[!IMPORTANT]

共享资源:多个线程能够看到的资源。

我们需要对共享资源进行保护。

锁的接口
// 互斥锁的类型:pthread_mutex_t
// 锁是全局的或者静态的,只需要initializer即可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
// 锁是动态的
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
对临界资源保护的理解

[!IMPORTANT]

所谓的对临界资源进行保护,本质是对临界区代码及进行保护!—> 我们对所有资源进行访问,本质都是通过代码进行访问的 —> 保护资源,本质就是把访问资源的代码进行保护

image-20240823002347391

原理角度理解锁

[!IMPORTANT]

如何理解申请锁成功,允许你进入临界区。—申请锁成功,pthread_mutex_lock()函数会返回。

如何理解申请锁失败,不允许你进入临界区。—申请所失败,pthread_mutex_lock()函数不返回,线程就是阻塞。线程会在每次时间

片中检查,临界资源是否仍被加锁,如果没有,则申请加锁,成功,就继续执行。

实现角度理解锁

[!IMPORTANT]

大多数的体系结构中,都提供了swap或者exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,只有一条指令,保证了原子性。

1、cpu的寄存器只有一套,被所用的线程共享,但是寄存器里面的数据属于执行流的上下文,属于执行流私有的数据。

2、cpu在执行代码的时候,一定也要有对应的执行载体—线程&&进程

3、数据在内存中,被所有线程是共享的。

结论:把数据从内存移动到cpu寄存器中,本质是把数据从共享变成线程私有!

饥饿问题:某些线程长时间在等待队列中等待资源

[!IMPORTANT]

解决方案:要二次申请时,必须排队。—具有一定的顺序性(同步)

抢票小程序
// ticket.cc
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <vector>
#include <mutex>
#include <memory>
#include "LockGuard.hpp"

int g_ticket_num = 10000;
int g_cnt = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void *func(void *args)
{
   
    while (g_ticket_num > 0)
    {
   
        LockGuard lockguard(&mtx);
        if (g_ticket_num > 0)
        {
   
            std::cout << g_ticket_num << "票" << std::endl;
            g_ticket_num--;
        }
    }
    return nullptr;
}
int main()
{
   

    std::vector<pthread_t> tids(10);

    for (int i = 0; i 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仍有未知等待探索

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值