【Linux】线程概念

1.线程的概念

1.1 什么是线程

  • 在⼀个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序列"
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

真正理解线程,就必须搞清楚,内核是如何进行资源划分的,根据代 码理解

1.2 分页式存储管理

页目录结构

1.3 优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的少作要少很多
  1. 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出
  2. 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制 。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有⼀个显著的区别是当改变虚拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中不会出现这个问题,还有硬件cache
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

1.4 缺点

  • 性能损失:每个线程都需要分配独立的栈空间(默认 1-2MB)、线程控制块(TCB)等内核资源,频繁创建 / 销毁线程(如每次请求都 new Thread ())会显著增加 CPU 和内存开销
  • 健壮性降低:任意一个线程崩溃,都会导致进程崩溃
  • 缺乏访问控制
  • 编程难度提高

1.5 线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 进程终止,该进程内的所有线程也就随即退出

2. Linux进程VS线程

2.1 线程有独立数据

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分"私有"数据:
  1. 线程ID
  2. 一组寄存器,线程的上下文数据 ----> 线程是独立调度的
  3. 栈 ------> 线程是动态的概念
  4. errno
  5. 信号屏蔽字
  6. 调度优先级

2.2 进程的多个线程共享

因为在同一地址空间,因此 Text Segment 、 Data Segment 都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(各种信号或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

3. Linux线程控制

3.1 POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,大多数函数的名字都是以"pthread_"打头的
  • 要使用这些函数库,要通过引入头文件pthread.h
  • 链接 这些线程函数库时要使用编译器命令的"-lpthread "选项

3.2 创建线程

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
  • pthreads函数出错时不会设置全局变量errno(大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以⽀持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
cpp 复制代码
#include "pthread.h"
#include "iostream"
#include "unistd.h"

void *threadrun(void *args)
{
    std::string name = (const char *)args;
    while (true)
    {
        sleep(1);
        std::cout << "我是新线程:name:" << name << ",我的pid:"
          << getpid() << std::endl;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadrun, (void *)"thread01");
    while (true)
    {
        std::cout << "我是主线程,我的pid是" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
cpp 复制代码
#include <pthread.h>
// 获取线程ID 
pthread_t pthread_self(void);

打印出来的 tid 是通过 pthread库中有函数pthread_self得到的,它返回一个 pthread_t 类型的变量,指代的是调用pthread_self 函数的线程的 "ID"

怎么理解这个"ID"呢?这个"ID"是pthread库给每个线程定义的进程内唯一标识,是pthread库维持的

由于每个进程有自己独立的内存空间,故此"ID"的作用域是进程级而非系统级(内核不认识)

其实pthread库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯一的"ID"来唯一标识这个线程

思考:为什么要有thread库

3.3 线程终止

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

  1. 线程的入口函数,进行return就是终止线程**(注意:线程不能用exit( )终止,因为exit是终止进程的!)**
  2. 线程可以调用pthread_exit终止自己
  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程
cpp 复制代码
原型:
      void pthread_exit(void *value_ptr);
功能:线程终⽌
参数:
      value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
 ⽆返回值,跟进程⼀样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的, 不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

cpp 复制代码
功能:取消⼀个执⾏中的线程
原型:
      int pthread_cancel(pthread_t thread);
功能:取消⼀个执⾏中的线程
参数: thread:线程ID
返回值:成功返回0;失败返回错误码

3.4 线程等待

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  • 创建新的线程不会复用刚才退出线程的地址空间

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过 pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值
  2. 如果thread线程被别的线程调用pthread_cancel异常终掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给 pthread_exit的参数
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
    printf("thread 1 returning ... \n");
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    return (void *)p;
}
void *thread2(void *arg)
{
    printf("thread 2 exiting ...\n");
    int *p = (int *)malloc(sizeof(int));
    *p = 2;
    pthread_exit((void *)p);
}
void *thread3(void *arg)
{
    while (1)
    { //
        printf("thread 3 is running ...\n");
        sleep(1);
    }
    return NULL;
}
int main(void)
{
    pthread_t tid;
    void *ret;
    // thread 1 return
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);
    // thread 2 exit
    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);
    printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
    free(ret);

    // thread 3 cancel by other
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n",
               tid);
    else
        printf("thread return, thread id %X, return code:NULL\n", tid);
}
cpp 复制代码
运⾏结果:
      thread 1 returning ... 
      thread return, thread id 5AA79700, return code:1
      thread 2 exiting ...
      thread return, thread id 5AA79700, return code:2
      thread 3 is running ...
      thread 3 is running ...
      thread 3 is running ...
      thread return, thread id 5AA79700, return code:PTHREAD_CANCELED

3.5 分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
  • 如果主线程不想再关心新线程,当新线程结束的时候,可以把他设置为分离状态(!joinable or detach),让他自己释放
  • 分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程依旧可以访问,主线程不用再等待新线程

线程组内其他线程对目标线程进行分离:

cpp 复制代码
int pthread_detach(pthread_t thread);

也可以是线程自己分离:

cpp 复制代码
pthread_detach(pthread_self());

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

joinable( ) 是std::thread 类的核心成员函数,返回一个布尔值,本质是判断「线程对象」和「操作系统底层线程」是否还保持有效关联。

4. 线程ID及进程地址空间布局

clone( )是Linux 内核提供的底层系统调用 ,专门用于创建新的进程 / 线程 ,是fork( )的升级版,也是 Linux 线程的底层实现基础

cpp 复制代码
int clone(
    int (*fn)(void *),   // 新进程/线程的入口函数
    void *child_stack,   // 新执行单元的栈空间
    int flags,           // 核心:资源共享标志位
    void *arg,           // 传给入口函数的参数
    ...                  // 可选参数
);

flag标志位:
CLONE_VM        // 共享内存空间(线程核心!)
CLONE_FS        // 共享文件系统信息
CLONE_FILES     // 共享文件描述符
CLONE_SIGHAND   // 共享信号处理
CLONE_THREAD    // 放入同一线程组(同PID)

5. 线程栈

虽然Linux将线程和进程不加区分的统一到了 task_struct ,但是对待其地址空间的 stack 还是有区别的

  • 对于Linux进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝(cow)以及动态增长。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯一可以访问未映射页而不一定会发生段错误⸺⸺超出扩充上限才报
  • 对于主线程生成的子线程,其 stack 将不再是向下生长的,而是事先固定下来的。线程栈一般是调用glibc/uclibc等的 pthread 库接口pthread_create 创建的线程,在文件映射区(或称之为共享区)
  • 这种stack不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。在glibc中通过mmap得到了stack之后,底层将调用sys_clone 系统调用:
cpp 复制代码
int sys_clone(struct pt_regs *regs) 
{ 
 unsigned long clone_flags; 
 unsigned long newsp; 
 int __user *parent_tidptr, *child_tidptr; 
 
 clone_flags = regs->bx; 
 //获取了mmap得到的线程的stack指针  
 newsp = regs->cx; 
 parent_tidptr = (int __user *)regs->dx; 
 child_tidptr = (int __user *)regs->di; 
 if (!newsp) 
 newsp = regs->sp; 
 return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, 
child_tidptr); 
} 
  • 对于子线程的 stack ,它其实是在进程的地址空间中map出来的一块内存区域,原则上是线程私有的,但是同一个进程的所有线程生成的时候,是会浅拷贝生成者的 task_struct 的很多 字段,如果愿意,其它线程也还是可以访问到的

6. 线程封装

Thread.cpp

cpp 复制代码
#include <iostream>
#include <string.h>
#include <cstdio>
#include <stdint.h>
#include <string>
#include <pthread.h>
#include <functional>

namespace ThreadModlue 
{

    static uint32_t number = 1;
    class Thread
    {
        using fun_t = std::function<void()>;

    private:
    //非静态函数有this指针参数,造成参数不匹配
        static void *Routine(void *args)
        {
            Thread* self=static_cast<Thread*>(args);
            self->EnableRunning();
            if(self->_isdetach)
            {
                self->Detach();
            }
            pthread_setname_np(pthread_self(),self->_name.c_str());
            self->_fun_c();
            return nullptr;
        }

    public:
        Thread(fun_t func)
            : _tid(0), _isdetach(false), _isrunning(false)
            ,res(nullptr)
            ,_fun_c(func)

        {
            _name = "thread-" + std::to_string(number++);
        }

        void Detach()
        {
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            //_isdetach=true;更规范
            EnableDetach();
        }

        void EnableDetach()
        {
            std::cout << "线程被分离了" << std::endl;
            _isdetach = true;
        }

        void EnableRunning()
        {
            _isrunning = true;
        }
        bool Start()
        {
            if(_isrunning)
            return false;
            int n = pthread_create(&_tid, nullptr, Routine, this);
            if (n != 0)
            {
                std::cerr << "create thread error" << std::endl;
                return false;
            }
            else
            {
                std::cout << _name << " create success" << std::endl;
                return true;
            }
            return false;
        }

        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    std::cerr << "create thread error: " << strerror(n) << std::endl;
                    return false;
                }
                else
                {
                    _isrunning = false;
                    std::cout << _name << " stop" << std::endl;
                    return true;
                }
            }
            return false;
        }

        void Join()
        {
            if (_isdetach)
            {
                std::cout << "你的线程已经是分离的了,不能进行join" << std::endl;
                return;
            }
            int n = pthread_join(_tid, &res);
            if (n != 0)
            {
                std::cerr << "create thread error: " << strerror(n) << std::endl;
            }
            else
            {
                std::cout << "join success" << std::endl;
            }
        }

        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        std::string _name;
        bool _isdetach;
        bool _isrunning;
        void *res;
        fun_t _fun_c;
    };
}

Main.cc

cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>
using namespace ThreadModlue;

int main()
{
    Thread t([]()
             {
  while(true)
{
    char name[128];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    std::cout<<"我是一个新线程:"<<name<<std::endl;
    sleep(1);
} });

    t.Start();
    t.Detach();
    sleep(5);
    t.Join();

    return 0;
}

注:

  1. pthread_setname_np 和 pthread_getname_np是两个用于设置和获取线程名称的非标准函数
  2. 线程名称长度限制:在Linux上,线程名称的最大长度为16个字符(包括结尾的\0 )。如果名称超过这个长度,会被截断
相关推荐
夜听莺儿鸣8 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断
杨云龙UP8 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
向成科技9 小时前
XC3576H工控主板|深度适配Ubuntu 26.04 LTS,释放边缘AI与工业开发新潜能
linux·人工智能·ubuntu·机器人·硬件·主板·边缘ai
Fcy64810 小时前
Linux下 进程间关系与守护进程
linux·运维·服务器·守护进程
码农小韩10 小时前
Linux驱动理论(二)——Linux字符设备驱动
linux·嵌入式软件开发·linux操作系统·linux应用开发·linux驱动理论
well061210 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班10 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了11 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP11 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc11 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu