【linux】线程概念与控制

发布于:2025-03-23 ⋅ 阅读:(21) ⋅ 点赞:(0)

引言

        当现代CPU的晶体管密度逼近物理极限,多核架构已成为突破性能瓶颈的必由之路。在这个计算密集型任务与异步IO需求并行的时代,多线程编程不再是可选项,而是开发者必须掌握的核心技能。Linux作为承载着全球90%云计算负载的操作系统,其线程实现机制既凝聚着UNIX哲学的精髓,又蕴含着处理器架构演进的智慧。

线程概念

通俗的讲:线程叫做进程内的一个执行流,CPU调度的基本单位。我们所说过一个进程是可以把自己的代码划分成一个部分,让另外的一个执行流去执行。之前我们是通过fork创建子进程然后用if判断,然后父子进程可以执行不同的代码块或者执行流(有写时拷贝)。所以一个进程是可以把自己的一部分资源交给另外一个执行流,让他去执行。如果我们今天把这个进程的代码区、已初始化、未初始化,堆区、共享区、栈区等等...尤其是代码区把它拆分成若干个不同的子区域,让我们当前进程去执行,相当于把页表分为若干部分让我们对应的当前的这个进程去使用。而这次在创建“进程”的时候,只创建一个的PCB,它最终指向的地址空间不再独立创建,而是和附近程指向同一个虚拟地址空间。我们把这种只创建PCB来让我们从父进程当中给它分配资源的这种执行流,我们就可以称其为叫做线程。

有了对进程,文件..的理解,操作系统肯定要设计专门的内核数据结构来对线程进行管理。这个结构一般称之为TCB(thread control block叫做线程控制块)。Linux的设计者认为,进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和算法直接复用代码,使用进程来模拟线程。所以Linux内核没有真正意义上的线程,没有为线程专门设计单独的数据结构(windows有),是用pcb来模拟线程,是一种完全属于自己的一套线程方案。

CPU调度的时候以LWP为标识符表示一个特定的执行流。创建一个线程其实就是在进程中创建一个pcb,每一个pcb都有一个唯一标识lwp。操作系统调度的时候只看lwp而不看pid,pid只是系统划分资源一种统一的方式。

现在我们就可以对于进程有了一个新的认识。概念重构:什么是进程?进程就是一堆pcb一个地址空间一个页表还有对应物理内存的一部分,这个整体我们叫做一个进程。他是承担分配系统资源的基本实体。进程用来整体申请资源,线程用来伸手向进程要资源。站在CPU的视角每一个pcb(task_struct)都可以称之为轻量级进程。操作系统和用户只认线程,因为Linux没有线程这样的概念只有轻量进程的概念,所以Linux无法直接提供创建线程的系统调用接口,只能给我们提供创建轻量级进程的接口。Linux没有创建线程的系统调用接口,这个接口是由库提供给我们的。我们需要与这个原生线程库建立链接,这个库叫做pthread库。下面我们来看一看这个库在哪以及底层创建线程的系统调用函数。

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

using namespace std;

int g_val = 0;

// std::ostream fun()
std::string fun()
{
    return "我是一个独立的方法";
    // std::iostream myos;
    // myos << "我是一个独立的方法";
    // return myos;
}

// 新线程
void *thread_routine(void *args)
{
    const char *name = (const char *)args;
    while (true)
    {
        fun();
        cout << "我是新线程, 我正在运行! name: " << name << " : "<< fun()  << " : " << g_val++ << " &g_val : " << &g_val << endl;
        sleep(1);
    }
}

int main()
{
    // typedef unsigned long int pthread_t;
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one");
    assert(0 == n);
    (void)n;

    // 主线程
    while (true)
    {
        // 地址 -> ?
        char tidbuffer[64];
        snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid);
        cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl;
        sleep(1);
    }

    return 0;
}

线程控制

线程创建

  • thread是输出型参数返回该线程的tid
  • attr是对于线程的属性设置(一般用默认设置传nullptr)
  • start_routine是一个函数指针(线程去执行这个可重入函数)
  • arg是传入函数的参数
  • 创建成功返回0,不成功返回错误码。

大部分系统调用接口如果不成功会将错误码保存在全局变量errno中,关于线程函数的调用一般成功返回0,不成功返回错误码,为什么不创建一个全局变量errno,保存在errno上呢?因为线程之间的数据共享,多线程之间去访问那个全局变量,我们缺乏对数据进行有效的访问控制而带来一些问题。

线程等待

线程也是需要被等待的。如果不等待会造成类似僵尸进程的问题,内存泄漏。线程退出可以获取线程的退出信息。回收线程对应的pcb等内核资源防止内存泄漏。把线程进行join,操作系统会自动去回收曾经进程创建的轻量级进程相关的资源,把资源进回收之后就进行释放。

线程中止

在线程函数中调用pthread_exit或者return直接退出或者调用线程取消函数pthread_cancel(tid),线程要被取消,前提是这个线程已经跑起来了。线程如果是被取消的,他的退出码是-1.

exit不能用来终止线程,因为exit是用来终止进程的,任何一个执行流调用exit都会让整个进程退出。特别的,一个线程如果出现了异常,会影响去他线程。因为线程健壮性或者鲁棒性较差,进程信号是整体发给进程的。

线程分离

int pthread_detach(pthread_t thread);

pthread_t  pthread_self() //获取线程tid

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
     #include <iostream>
     #include <cstdlib>
     #include <string>
     #include <cassert>
     #include <vector>
     #include <pthread.h>
     #include <unistd.h>
    
     using namespace std;
    
      当成结构体使用
     class ThreadData
     {
     public:
         int number;
         pthread_t tid;
         char namebuffer[64];
     };
    
     class ThreadReturn
     {
     public:
         int exit_code;
         int exit_result;
     };
    
     1. start_routine, 现在是被几个线程执行呢?10, 这个函数现在是什么状态?重入状态
     2. 该函数是可重入函数吗?是的!
     3. 在函数内定义的变量,都叫做局部变量,具有临时性 -- 今天依旧适用 -- 在多线程情况下, 也没有问题 -- 其实每一个线程都有自己独立的栈结构!
     void *start_routine(void *args)
     {
          sleep(1);
          一个线程如果出现了异常,会影响其他线程吗?会的(健壮性或者鲁棒性较差)
          为什么?进程信号,信号是整体发给进程的!
    
         ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
         int cnt = 10;
         while (cnt)
         {
             cout << "cnt: " << cnt << " &cnt: " << &cnt << endl; // bug
             cnt--;
             sleep(1);
              return nullptr;
              pthread_exit(nullptr);
              exit(0); // 能不能用来终止线程,不能,因为exit是终止进程的!,任何一个执行流调用exit都会让整个进程退出
              cout << "new thread create success, name: " << td->namebuffer << " cnt: " << cnt-- << endl;
              int *p = nullptr;
              // p = nullptr;
              *p = 0;
         }
    
          线程如何终止的问题
          delete td;
          pthread_exit(nullptr);
          return nullptr; // 线程函数结束,return的时候,线程就算终止了
    
          return (void*)td->number; // warning, void *ret = (void*)td->number;
          return (void *)106;
          pthread_exit((void*)111); // 既然假的地址,整数都能被外部拿到,那么如何返回的是,堆空间的地址呢?对象的地址呢?
         ThreadReturn * tr = new ThreadReturn();
         tr->exit_code = 1;
         tr->exit_result = 106;
    
          ThreadReturn tr; // 在栈上开辟的空间 return &tr;
    
         return (void*)100; //右值
     }
    
     int main()
     {
          1. 我们想创建一批线程
         vector<ThreadData*> threads;
     #define NUM 10
         for(int i = 0; i < NUM; i++)
         {
             ThreadData *td = new ThreadData();
             td->number = i+1;
             snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
             pthread_create(&td->tid, nullptr, start_routine, td);
             threads.push_back(td);
    
              pthread_create(&tid, nullptr, start_routine, (void*)"thread one");
              pthread_create(&tid, nullptr, start_routine, namebuffer);
              sleep(1);
         }
    
         for(auto &iter : threads)
         {
             cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
         }
          线程是可以被cancel取消的!注意:线程要被取消,前提是这个线程已经跑起来了
          线程如果是被取消的,退出码:-1
          PTHREAD_CANCELED;
         sleep(5);
         for(int i = 0; i < threads.size()/2; i++)
         {
             pthread_cancel(threads[i]->tid);
             cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
         }
    
         for(auto &iter : threads)
         {
             void *ret = nullptr; // 注意: 是void *哦
              ? : 为什么没有见到,线程退出的时候,对应的退出信号??? 线程出异常,收到信号,整个进程都会退出!
              pthread_join:默认就认为函数会调用成功!不考虑异常问题,异常问题是你进程该考虑的问题!
             int n = pthread_join(iter->tid, (void**)&ret); // void ** retp; *retp = return (void*)td->number
             assert(n == 0);
             cout << "join : " << iter->namebuffer << " success, exit_code: " << (long long)ret << endl;
             delete iter;
         }
    
         cout << "main thread quit " << endl;
    
          while (true)
          {
              cout << "new thread create success, name: main thread" << endl;
              sleep(1);
          }
    
          pthread_t id;
          pthread_create(&id, nullptr, start_routine, (void *)"thread new");
    
          while (true)
          {
              cout << "new thread create success, name: main thread" << endl;
              sleep(1);
          }
    
         return 0;
     }

线程的私有资源

线程一旦被创建,几乎所有的资源都是被所有线程共享的(代码和全局数据还有进程文件描述符表)。线程一定要有自己的私有资源。什么资源应该是线程私有的呢?pcb属性私有,上下文结构,每一个线程都要有自己独立的栈结构(保存局部变量)。因为线程是动态切换的,如果我的代码没有运行完,需要保存上下文数据和代码,然后进行线程切换,等切换回来后继续向下执行。在函数内定义的变量,叫做局部变量,具有临时性。可以证明每个线程都有自己独立的栈结构。

线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程缺点

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

面试题

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多的原因(面试题)

  1. 1.进程要切换需要:切换页表 切换虚拟地址空间 切换pcb 切换上下文
  2. 2.线程切换:切换pcb和上下文
  3. 3. 软件存在一种属性叫做局部性原理,当前正在访问的代码或者数据附近的代码非常有较大的概率被访问到的热点数据,当前你正在访问第一万行代码,我们就可以先把十万行以后的代码先切出去。
  4. 计算机页面置换的原理:如果当前一个进程内部正在进程处理,他的代码和数据会预先或者整体被放到了cache当中。然后cpu在进行读取时,他不会中间访问内存而是现在cache中读取,如果没有命中CPU会再次从内存中读取,先缓存到cache里...。线程切换cache不用太更新,但是进程切换全部要更新。这就是与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多的原因。热点数据就是经常要被访问的数据(被当前进程以较高的概率命中的 这里对热点数据进行判断需要时间):计算密集型应用(CPU,加密,解密,算法等) 、I/O密集型应用(外设,访问磁盘显示器网络)。

线程tid的含义

线程库实现了对线程的管理,比操作系统实现要简单的多,所以线程库是怎么实现的呢?他直接有一个线程就创建一个这样的我们的线程的结构体,我们可以简单称之为叫做TCB(thread control block)。线程库会创建一个thread* []threads数组去管理线程。为了更好的去找到某一个线程,我们只需要找到它数组的起始位置就可以。所以线程的TID对应的就是这一块或者这一块在库当中的起始地址。

结语

Linux采用进程模拟线程,通过PCB(进程控制块)而非专用TCB管理线程,形成轻量级进程(LWP)。每个线程对应独立PCB,共享进程的虚拟地址空间(代码、数据、堆等),线程栈为私有资源。CPU调度以LWP为标识符,操作系统仅识别线程级执行流,进程作为资源分配实体,线程通过共享资源提升效率。Linux线程模型以轻量级进程为核心,平衡效率与实现复杂度,在多核时代成为高性能计算基石,但其共享资源机制对开发者线程安全提出更高要求。理解其底层机制是优化多线程程序的关键。