【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;
}

本篇完,感谢阅读。

相关推荐
liuyao_xianhui2 小时前
优选算法_岛屿数量_floodfill算法)_bfs_C++
java·开发语言·数据结构·c++·算法·链表·宽度优先
芯盾时代2 小时前
金融行业AI治理与安全解决方案
人工智能·安全·金融
u86882 小时前
Maixin AICC智能呼叫中心:以AI语音助力新能源车企优质客服
人工智能·大模型电话对接·ai语音智能体
CS创新实验室2 小时前
AI时代社会与职业变迁系统综述
人工智能·百度
翼龙云_cloud2 小时前
阿里云代理商:OpenClaw 技能安全部署指南与高口碑扩展精选
人工智能·安全·云计算·openclaw
我材不敲代码2 小时前
OpenCV 实现人脸识别全流程:从人脸检测到 LBPH/Eigen/Fisher 三种算法实战
人工智能·opencv·计算机视觉
oh LAN2 小时前
主流 AI 编码工具对比表(2026 最新)
人工智能·编辑器·工具·代码
这张生成的图像能检测吗2 小时前
(论文速读)嵌入式GPU上的实时多目标视觉追踪
人工智能·深度学习·目标检测·目标跟踪·iot边缘设备
悟乙己2 小时前
能够替代 Claude Code 的本地大语言模型选项推荐
人工智能·语言模型·自然语言处理