【Linux系统】入门线程:线程介绍与线程控制

文章目录

  • 一、线程是什么
    • [1. 简介](#1. 简介)
    • [2. 特点](#2. 特点)
    • [3. 线程与进程对比](#3. 线程与进程对比)
    • [4. 可重入函数](#4. 可重入函数)
  • 二、线程控制
    • [1. 创建线程](#1. 创建线程)
    • [2. 线程终止](#2. 线程终止)
    • [3. 线程等待](#3. 线程等待)
    • [4. 线程分离](#4. 线程分离)
    • [5. 线程id与线程地址空间的理解](#5. 线程id与线程地址空间的理解)
    • [6. 简单的封装线程控制](#6. 简单的封装线程控制)

一、线程是什么

1. 简介

在一个程序里的一个执行流叫做线程,更准确的定义是"一个进程内部的控制序列"。

在CPU的视角内,没有进程,只有执行流(线程)!

之前我们学习的进程,本质只有一个线程,即只有一个执行流。实际上,一个进程可以有多个线程,至少有一个。
进程是资源分配的基本单位,而线程是操作系统调度和执行的基本单位!
进程 = 多个线程+虚拟地址空间+页表+代码和数据

既然一个进程可以有多个线程,那么势必要对线程进程描述组织,所以一定有相关的数据结构!Windows仿照进程控制块PCB,单独设计了一种线程控制块TCB;而Linux中,线程复用了进程的task_strcut数据结构进行描述!

所以,严谨来说,Windows才存在真正的线程;Linux中不存在真正意义上的线程,应该称之为"轻量级进程"!
Linux提供了轻量级进程相关的系统调用,可是我们用户只想用线程怎么办?于是有了pthread库------用户级线程库,为我们提供管理线程的接口和参数,向下调用Linux的轻量级进程系统调用。

pthread是一个第三方库,早期C库没有包含他,编译时需要我们显示链接-lpthread,现在新版本的C库可能已经包括他了。

为了方便叙述,本文中暂且认为:Linux轻量级进程==线程。

2. 特点

首先要知道的是:
一个进程的所有线程共享一份地址空间与页表!也就是共享进程资源!

由于这一点,创建新线程时不需要拷贝地址空间和页表,创建新进程。所以创建一个新线程的代价比创建一个新进程小的多

同时,因为进程切换需要丢弃cache,线程切换不需要切换cache。与进程切换相比,线程的切换需要操作系统做的工作少很多

在计算密集型应用中,为了能在多核多CPU机器上效率更高,可以将计算分解到多个线程中实现。

线程也有缺点:最主要的是缺乏保护,因在时间分配上的细微差别或者资源使用冲突的可能性是很大的。这就需要线程互斥和线程同步操作了。

当然,要记得所有线程都是属于同一个进程的。单独某个线程出现异常崩溃或发送信号等情况,影响的是整个进程所有线程,进程挂掉了所有线程也就都挂了。

3. 线程与进程对比

进程具有独立性,大部分资源是独占的。

一个进程的所有线程共享虚拟地址空间,也就共享大部分进程资源。

因为是同一个地址空间,所以代码段和全局数据段都是共享的。一个全局变量、全局函数,各进程都能访问。除此之外,各线程还共享:

  • 文件描述符表
  • 信号动作表
  • 当前工作目录
  • 进程pid和用户id
  • 等等

但是,也有一些资源是每个线程单独拥有的:

  • 线程tid
  • 线程自己的上下文数据
  • 函数栈帧
  • 线程局部存储(后面讲)
  • errno
  • 信号屏蔽字
  • 调度优先级
  • 等等

4. 可重入函数

可重入函数是指可以被多个任务或执行流(如中断处理、多线程)同时安全调用的函数。

其核心在于:当函数正在执行时,另一个执行流(如中断或另一线程)同时再次进入该函数,不会产生数据错乱或逻辑错误。

这意味着,可重入函数,不能调用全局资源,如:

  • 不能调用malloc或free
  • 不能访问全局或静态变量
  • 不能调用标准IO函数

这种函数就是不可重入函数。

反之,如果一个函数只访问自己的局部变量或参数,则称之为可重入函数。

在后续讲解线程互斥、线程同步时的线程安全问题,函数是否可重入是一个重要的问题。

二、线程控制

与线程有关的函数基本都属于pthread库,使用这些函数,需要包含头文件<pthread.h>,gcc编译时需要使用选项-lpthread

以下所有函数,返回值为int类型都表示:函数调用成功返回0,失败返回错误码。

命令ps -aL可以查到当前所有的轻量级进程。

1. 创建线程

参数:

  • thread:输出型参数,记录创建的新线程id。
  • attr:用来设置线程属性,我们用户不必关心,传递NULL即可。
  • start_routine:是一个参数为void*,返回值为void*的函数,是创建的新线程会去执行的函数。
  • arg:传递给start_routine的参数。

2. 线程终止

如果要只终止一个线程而不终止整个进程,有三种方法:

  • 线程函数内return

  • 线程调用pthread_exit终止自己

    这个函数的参数相当于线程的返回值

  • 线程调用pthread_cancel可以终止同一进程中的任意一个进程(不建议使用)

需要注意的是,不论return还是pthread_exit,返回的指针指向的内存必须是全局的或是手动分配的。不能在线程函数的栈上分配,否则线程函数退出后这块内存会被自动释放了。

3. 线程等待

进程需要被等待,是因为他需要被父进程回收,防止内存泄露,获得退出信息,否则会导致子进程的僵尸问题。
线程等待的道理也是类似的,主线程需要回收子线程的空间,也可能需要获得新线程的执行结果。

除此之外,一个子线程的异常崩溃会杀死整个进程;而子进程的崩溃,父进程可以捕获并继续运行。

等待线程的函数:

参数:

  • thread:等待的线程id
  • retval:一个二级指针,它指向的指针会指向线程的返回值。如果不关心线程的终止信息,可以设为NULL

如果thread线程是被别的线程用pthread_cancel异常终止的,则retval指向的内容是常数PTHREAD_CANCELED,即-1。

4. 线程分离

默认情况下,新创建的线程是"joinable"的,这代表线程退出后必须对其进行线程等待操作,否则无法释放资源。
如果不关心线程的退出情况,不需要等待线程,我们可以将线程设置为"分离状态",让线程退出时系统自动回收释放线!

但是,一旦子线程被设置为分离状态,主线程就不能提前退出。

pthread_detach函数,用于分离一个线程:

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

5. 线程id与线程地址空间的理解

pthread库会给每个线程分配一个线程id,这个id是pthread库给每个线程定义的进程内唯一标识,此id的作用域是进程级而非内核级!

Linux系统中,pthread_t 类型的线程id,本质是一个地址,是一个进程地址空间上的地址!

如图,这是线程在进程地址空间的分布情况:
pthread动态库链接到内存共享区,内部会给每个线程开辟一部分区域,存放每个线程自己的数据。线程id,就是每个线程自己的内存区域的起始一个字节地址!

函数pthread_self,能返回当前线程的id:

如果一个全局变量用__thread修饰了,则各个线程会各自开辟一份空间,各自有一份·。互不干扰。这种就称之为线程局部存储!
要注意的是,只能用来局部存储内置类型数据。

除此之外,我们还可以在系统层面给线程自己设定名字,名字不能过长否则会设置失败

综合演示:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

// 局部存储, 每个线程自己有独立的a变量
__thread int a = 10;
// 正常的全局变量,所有线程都能看到并共享
int b = 1;

void* routine(void* args)
{
    pthread_t* id = static_cast<pthread_t*>(args);
    std::string name = "thread" + std::to_string(b++);
    pthread_setname_np(*id, name.c_str());

    while (1)
    {
        std::cout << "新线程id: " << *id << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid1, tid2;
    // 创建两个新线程,新线程调用routine函数
    pthread_create(&tid1, nullptr, routine, (void*)&tid1);
    pthread_create(&tid2, nullptr, routine, (void*)&tid2);

    pthread_detach(tid2);        // 分离tid2,系统会自动回收
    pthread_join(tid1, nullptr); // 等待回收线程tid1

    return 0;
}

打印线程信息中的LWP,可以认为是真正的"轻量级进程id",之前所说的pthread_t 类型的线程id本质就是虚拟空间中的一个地址。
有一个线程的LWP和进程pid相同,这个线程就是主线程。主线程的栈就在整个内存空间的栈上,其他子线程的栈在共享区。

6. 简单的封装线程控制

cpp 复制代码
// Thread.hpp
#ifndef _THREAD_
#define _THREAD_

#include <iostream>
#include <pthread.h>
#include <unistd.h>

static int gnumber = 1;
// 想要让线程执行的任务类型,但是线程函数必须是void*(*)(void*)类型,
// 所以要在线程函数内再封装真正想要让线程完成的任务
using callback_t = void (*)();

class Thread
{
private:
    // 类内普通成员函数会隐藏this参数,因此必须加static防止隐式传this
    static void* Thread_Routine(void* args)
    {
        Thread* self = static_cast<Thread*>(args);
        pthread_setname_np(self->_tid, self->_name.c_str());
        self->_task(); // 执行任务
        return nullptr;
    }

public:
    Thread(callback_t task) : _task(task), _tid(-1), _joinable(true), _result(nullptr)
    {
        _name = "NewThread-" + std::to_string(gnumber++);
    }

    void Start()
    {
        // 我们想要在Thread_Rontine函数类调用_task完成任务,可以把this自己传过去
        pthread_create(&_tid, nullptr, Thread_Routine, this);
    }

    void Join()
    {
        if (_joinable)
        {
            pthread_join(_tid, &_result);
            std::cout << "线程已回收" << std::endl;
        }
        else
        {
            std::cerr << "线程不可被等待" << std::endl;
        }
    }

    void Detach()
    {
        if(_joinable)
        {
            pthread_detach(_tid);
            _joinable = false;
        }
        else
        {
            std::cerr<<"线程已被分离" << std::endl;
        }
    }

    ~Thread()
    {
    }

private:
    std::string _name;
    pthread_t _tid;
    callback_t _task;
    void* _result;
    bool _joinable;
};

#endif
cpp 复制代码
#include "Thread.hpp"

void task1()
{
    while (1)
    {
        std::cout << "这是task1" << std::endl;
        sleep(1);
    }
}

void task2()
{
    while (1)
    {
        std::cout << "这是task2" << std::endl;
        sleep(1);
    }
}

int main()
{
    Thread th1(task1);
    Thread th2(task2);

    th1.Start();
    th2.Start();

    th1.Join();
    th2.Join();

    return 0;
}

本篇完,感谢阅读。

相关推荐
奔跑中的小象3 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南3 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang3 小时前
LangGraph 状态机实战
人工智能
刹那芳华19923 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作4 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门4 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜4 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟4 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
爱丶狸4 小时前
Grafana_Zabbix_ImageRenderer_部署与前端操作手册
linux·前端·zabbix·grafana·kylin
天国梦4 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能