驾驭Linux多线程:控制、分离与高效管理的关键策略

Linux系列



前言

上篇文章,我们已经介绍了,线程的基本概念和实现,本篇我们来学习线程的创建和使用。


线程的基本概念

一、线程控制

在Linux系统中,内核并未为线程单独设计管理结构,而是通过复用进程的数据结构和调度机制,以「轻量级进程」(LWP)的形式模拟线程行为——所有执行实体在操作系统层面均由task_struct结构体管理,本质上都是轻量级进程,差异仅在于资源共享范围(如通过clone()系统调用的标志位控制是否共享虚拟内存、文件描述符等)。因此,Linux内核未向用户提供独立的线程系统调用,而是直接暴露轻量级进程的创建接口(即clone())。然而,用户层需要更抽象的线程编程模型,于是POSIX线程库(如pthread)在应用层对clone()进行封装,通过设置特定的资源共享标志(如CLONE_VM共享地址空间),将轻量级进程的底层实现抽象为「线程」概念,从而为开发者提供了符合直觉的线程创建、同步等高层接口。

clone :是 Linux 内核提供的一个强大且灵活的系统调用,它允许创建一个新的执行实体,并且可以通过一系列标志位来精确控制新实体与父实体之间共享哪些资源,而前面使用的fork()就是通过封装clone()实现的(这个系统调用我们并不深入研究,只会在后面进行简单了解)。

线程的创建

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr
,void *(*start_routine) (void *), void *arg);

功能:创建一个新的线程。

参数
thread:指向pthread_t类型的指针,用来存储创建的线程的标识符。

attr:用于指定线程的属性,如线程的栈大小,调度策略等,设置为null,表示使用默认属性。

start_routuie:函数指针,指向线程开始执行的函数。该函数需要接收一个void* 类型的参数,并返回一个void*类型的值。

arg:传递给start_routine函数的参数。

pthread_t:unsigned long int 类型。

示例1

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clear
clear:
	rm -rf mythread

在编译运用 pthread_create 函数的代码时,我们必须手动对 pthread 库进行链接。这是由于 pthread 库并非系统默认自动链接的标准库,而是需要额外引入的第三方库。

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

using namespace std;

void* handler(void*args)
{
    char *str=(char*)args;
    while(true)
    {
        cout<<str<<" :"<<getpid()<<endl;
        sleep(2);
    }
    return nullptr
}
int main()
{
    pthread_t tid;

    pthread_create(&tid,nullptr,handler,(void*)"new thread pid");
    while(true)
    {
        cout<<"main thread pid:  "<<getpid()<<endl;
        sleep(2);
    }
    return 0;
}

在上述代码中,我们创建了一个新线程,新线程与主线程均进入循环,每隔两秒输出一行信息,且验证两者共享同一进程 ID(通过 getpid() 获取)。
在这里插入图片描述

从输出结果可以看到,两个线程共享同一个ID,那么操作系统该如何区分它们呢?或者说在执行流切换时,如何判断何时进行线程间切换、何时进行进程间切换呢?

我们通过下面的指令来查看:

ps -aL

在这里插入图片描述
从程序执行结果可以看到,有两个PID为:718834的线程。当PID和LWP(轻量级进程ID)相等时,我们称它为主线程,不相等的为用户创建的新线程。

示例2

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

using namespace std;
int flag=0;
void* handler(void*args)
{
    while(true)
    {
        cout<<(char*)args<<flag<<endl;
        sleep(2);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;

    pthread_create(&tid,nullptr,handler,(void*)"new thread flag: ");
    cout<<"new thread tid:  "<<tid<<endl;
    int cnt=3;
    while(cnt--)
    {
        flag++;
        sleep(2);
    }
    cout<<"process quit"<<endl;
    return 0;
}

此程序用来验证,线程共用进程的地址空间、变量(flag),并且输出新线程的ID。
在这里插入图片描述

在这里插入图片描述

对比程序和指令的执行结果可发现,LWP(轻量级进程ID)与线程的ID并不相同。LWP是内核用于标识线程的唯一标识符,那么该如何理解线程的ID呢?

二、线程的管理

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, 
void *arg, ...)            

在这里插入图片描述

此函数不是重点
我们使用的forkpthread_create都是封装的该函数。

在内核层面并无线程的概念,但上层用户需要线程抽象能力。当创建线程时,线程库需为每个线程提供独立的执行逻辑与栈空间,其底层通过封装系统调用clone()实现:为新线程分配独立栈空间,从而形成执行上下文的隔离。因此,线程的概念由线程库负责维护——当使用原生线程库(如POSIX线程库pthread)时,库本身需加载到内存并通过页表映射至进程地址空间的动态链接区域(而非内核共享区),除主线程外的其他线程栈空间均在进程用户态地址空间内动态开辟。

由于操作系统仅将线程视为轻量级进程(LWP)进行调度,线程的属性(如优先级、同步状态、私有数据等)均由线程库自行管理。具体而言,线程库会为每个线程创建库级线程控制块(TCB),用于记录线程ID、执行状态、栈指针等信息,形成用户态的线程抽象。在此过程中,线程库无需干预内核对底层task_struct(轻量级进程控制块)的调度逻辑,仅需在用户层通过TCB完成线程生命周期管理(如创建、销毁、同步协调等)。
在这里插入图片描述
为了方便对线程的查找,设计者将每个线程TCB空间的起始地址,当作该线程的ID。我们通过指令看到的LWPOS用来区分不同线程的表示。

下面我们就以地址形式打印tid;

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

using namespace std;
void* handler(void*args)
{
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,handler,(void*)"new thread flag: ");
    printf("new thread tid:%p\n",(void*)tid);//以地址形式输出
    cout<<"process quit"<<endl;
    return 0;
}

在这里插入图片描述
这里大家可以验证一下,该地址是属于堆区、栈区之间的。
我们也可以在线程中,通过pthread_self()来获取:

  #include <pthread.h>
  pthread_t pthread_self(void);

功能:获取当前线程的ID

void* handler(void*args)
{
    printf("new thread tid: %p\n",(void*)pthread_self());
    return nullptr;
}
int main()
{
    pthread_t tid;

    pthread_create(&tid,nullptr,handler,(void*)"new thread tid : ");
    sleep(1);
    cout<<"process quit"<<endl;
    return 0;
}

在这里插入图片描述

三、线程等待

在编写多线程代码时,线程等待操作十分必要。首先,它能保证所有线程顺利执行完毕,确保程序按预期完成各项任务。其次,可有效避免资源竞争和数据不一致的问题,让多线程环境下的数据操作更加稳定可靠。再者,线程等待有助于回收线程资源,防止资源的浪费和滥用。另外,通过线程等待,我们还能获取线程的执行结果,以便根据结果进行后续的处理。

如果不进行线程等待,可能会引发一系列严重问题,内存资源泄漏便是其中之一。长期来看,这不仅会降低系统性能,还可能导致程序崩溃。

在实际编程中,通常使用pthread_join()接口来实现线程等待,它能让主线程暂停执行,直到指定的线程执行完毕,从而保证程序的正确性和稳定性。

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

功能:阻塞当前线程,直到指定的线程终止,回收已终止的线程资源,并获取其返回值。

参数

  • thread:要等待线程的ID

  • retval:用于存储被等待线程的返回值的指针。若不关心设为nullptr

返回值
成功返回0,失败返回错误码

void* handler(void*args)
{
    printf("new thread tid: %p\n",(void*)pthread_self());
    sleep(2);
    return (void*)"hhh";
}
int main()
{
    pthread_t tid;

    pthread_create(&tid,nullptr,handler,(void*)"new thread tid : ");
    void*retval;
    pthread_join(tid,&retval);
    cout<<(char*)retval<<endl;
    sleep(1);
    cout<<"process quit"<<endl;
    return 0;
}

在这里插入图片描述
一般我们的线程是通过返回自定义类,来完成执行信息的返回的,这里为了方便展示,使用的都是比较简单的例子。

四、线程分离

线程分离属于线程的一项属性设置,当线程被设置为分离状态时,其在执行结束之际能够自行释放所占用的资源。这意味着无需其他线程借助pthread_join函数来等待该线程执行完毕并回收资源。

通常,当我们并不需要知晓线程的执行结果时,就会采用线程分离这种方式。通过设置线程分离属性,不仅能简化多线程编程的逻辑,还能避免因等待线程结束而带来的额外开销,从而提升程序的整体性能和资源利用率。

#include <pthread.h>
int pthread_detach(pthread_t thread);

功能:将指定线程设置为分离状态,线程执行结束后,系统回收该资源。

void* handler(void*args)
{
    printf("new thread tid: %p\n",(void*)pthread_self());
    sleep(2);
    return nullptr;
}
int main()
{
    pthread_t tid;

    pthread_create(&tid,nullptr,handler,(void*)"new thread tid : ");
    pthread_detach(tid);
    sleep(1);
    cout<<"process quit"<<endl;
    return 0;
}

线程也会有僵尸,但是这个场景展示不出来,大家结合进程知识感受吧

总结

本篇我们围绕Linux系统下的线程编程展开,核心内容涵盖线程的实现原理、创建、管理、等待及分离等方面。具体而言,Linux内核以“轻量级进程”模拟线程,复用进程数据结构与调度机制,POSIX线程库对clone()系统调用封装,为开发者提供线程编程接口。线程创建通过pthread_create实现,需手动链接pthread库;线程管理涉及clone系统调用及线程库对线程控制块(TCB)的维护;线程等待使用pthread_join,能确保线程执行完成、避免资源竞争与泄漏并获取执行结果;线程分离则用于无需获取线程结果的场景,可自动释放资源,减少编程复杂度与执行开销 。

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值