【Linux】线程概念与控制(2)

3. Linux 线程控制:POSIX 线程库的核心操作

Linux 中通过 **POSIX 线程库(pthread)** 实现线程控制,包括线程创建、终止、等待、分离等操作。这些操作是多线程编程的基础,本节详解其用法与原理。

3-1 POSIX 线程库基础

POSIX 线程库是 Linux 下多线程编程的标准接口,核心特点:

  • 函数命名 :线程相关函数以pthread_开头(如pthread_createpthread_join)。
  • 头文件 :需包含<pthread.h>
  • 编译链接 :pthread是第三方库,所以编译时需加-lpthread选项(链接 pthread 库)。

3-2 创建线程:pthread_create

功能

创建新线程,使其执行指定函数。

函数原型

cpp 复制代码
int pthread_create(
    pthread_t *thread,         // 输出参数:返回线程ID
    const pthread_attr_t *attr, // 线程属性(NULL表示默认属性)
    void *(*start_routine)(void*), // 线程执行的函数(函数指针)
    void *arg                   // 传递给线程函数的参数
);

返回值

  • 成功:返回0
  • 失败:返回错误码 (非-1,pthread 函数不设置errno)。

示例

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

void* threadrun(void* args)
{
    std::string name = (const char*)args;
    while(1)
    {
        std::cout << "我是新线程" << name << std::endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
    if(n != 0)
    {
        std::cout << "pthread_create error" << std::endl;
        return 1;
    }

    while(1)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
    return 0;
}
  • 内核线程 ID(LWP) :通过ps -aL查看,是内核调度的唯一标识(系统全局唯一)。

CPU调度的时候,看得实际是lwp。

3-3 线程终止

线程可通过以下 3 种方式终止,且仅终止自身(不影响进程内其他线程):

1. 线程函数return

线程函数执行到return时,线程自然终止(主线程return会导致进程退出)。

2. pthread_exit主动终止

cpp 复制代码
void pthread_exit(void *value_ptr);
  • value_ptr:线程退出时的返回值(需为全局 / 堆内存,不能是栈变量)。
  • 无返回值(线程自身无法 "返回" 到调用者)。

3. pthread_cancel取消其他线程

cpp 复制代码
int pthread_cancel(pthread_t thread);
  • 向目标线程发送 "取消请求",目标线程在取消点 (如sleepread等系统调用)响应并终止。
  • 返回值:成功0,失败错误码。

3-4 线程等待:pthread_join

在 POSIX 线程(pthread)中,若创建的非分离态线程未通过 pthread_join 等待回收,会导致僵尸线程,引发以下问题:

  1. 资源泄漏:线程结束后,内核中的线程控制块(TCB)不会释放,持续占用内核内存和线程描述符等稀缺资源。大量僵尸线程会耗尽系统线程资源,导致无法创建新线程。

  2. 内存泄漏 :线程分配的私有资源(如堆内存、文件描述符)若依赖 pthread_join 回收(例如通过线程返回值传递资源指针),不调用该函数会导致这些资源无法释放,造成用户态内存泄漏。

  3. 程序逻辑隐患pthread_join 可获取线程退出状态,不调用则主线程无法得知子线程是否正常结束、是否返回预期结果,可能引发逻辑错误。

功能

等待指定线程终止,回收其资源(类似进程的wait)。

函数原型

cpp 复制代码
int pthread_join(
    pthread_t thread,   // 要等待的线程ID
    void **value_ptr    // 输出参数:存储线程的退出状态
);

线程退出状态的获取

  • 若线程通过return退出:value_ptr存储线程函数的返回值。
  • 若线程被pthread_cancel终止:value_ptr存储PTHREAD_CANCELED
  • 若线程调用pthread_exitvalue_ptr存储pthread_exit的参数。

示例

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

void *thread1(void *arg) {
    int *ret = malloc(sizeof(int));
    *ret = 100;
    return (void *)ret; // return退出,返回值存于堆
}

void *thread2(void *arg) {
    int *ret = malloc(sizeof(int));
    *ret = 200;
    pthread_exit((void *)ret); // pthread_exit退出
}

void *thread3(void *arg) {
    while (1) sleep(1); // 无限循环,等待被取消
}

int main() {
    pthread_t tid;
    void *ret;

    // 测试return退出
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("Thread 1 return: %d\n", *(int *)ret);
    free(ret);

    // 测试pthread_exit退出
    pthread_create(&tid, NULL, thread2, NULL);
    pthread_join(tid, &ret);
    printf("Thread 2 exit: %d\n", *(int *)ret);
    free(ret);

    // 测试pthread_cancel退出
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(1);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if (ret == PTHREAD_CANCELED)
        printf("Thread 3 canceled\n");

    return 0;
}

3-5 分离线程:pthread_detach

背景

默认情况下,线程是joinable 的:若不调用pthread_join,线程终止后资源不会自动释放,导致 "僵尸线程"。

功能

将线程标记为分离态(detached) ,线程终止时自动释放资源,无需pthread_join

函数原型

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

两种分离方式

  1. 其他线程分离目标线程

    cpp 复制代码
    pthread_detach(tid); // tid为目标线程ID
  2. 线程自身分离

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

示例

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

void *thread_func(void *arg) {
    pthread_detach(pthread_self()); // 自身分离
    printf("Thread is running...\n");
    sleep(1);
    return NULL; // 终止后自动释放资源
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);

    sleep(2); // 等待线程执行
    // 无需pthread_join,线程已自动释放资源
    printf("Main thread exit\n");
    return 0;
}

总结

  • 创建pthread_create启动新线程,共享进程资源。
  • 终止returnpthread_exitpthread_cancel三种方式,灵活控制线程生命周期。
  • 等待pthread_join回收线程资源,获取退出状态。
  • 分离pthread_detach让线程自动释放资源,避免僵尸线程。

这些操作是 Linux 多线程编程的基础,需根据场景选择合适的线程控制方式。

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

线程 ID 存在两种不同的 "身份标识":一种是内核调度层面的线程 ID(LWP),另一种是线程库(NPTL)维护的用户态线程 ID。两者作用域和含义不同,需结合进程地址空间布局理解其本质。

4.1 两种线程 ID 的区分

1. 内核线程 ID(LWP)

  • 本质:内核级别的线程标识,是操作系统调度器识别线程的唯一标识(类似进程 ID,但针对线程)。
  • 特性
    • 系统全局唯一(整个系统中不会有两个线程拥有相同的 LWP)。
    • 属于进程调度范畴,内核通过 LWP 进行线程的调度和资源分配。
    • 可通过ps -aL命令查看(LWP列即为内核线程 ID)。
  • 示例:主线程的 LWP 与进程 ID(PID)相同,其他线程的 LWP 为独立数值。

2. 用户态线程 ID(pthread_t)

  • 本质:由 POSIX 线程库(NPTL,Native POSIX Thread Library)维护的线程标识,是用户态操作线程的 "句柄"。
  • 特性
    • 仅在进程内唯一 (不同进程可存在相同的pthread_t值)。
    • 属于线程库范畴,线程库的所有操作(如pthread_joinpthread_cancel)均依赖此 ID。
    • 通过pthread_self()函数获取当前线程的pthread_t
  • 实现细节 :在 Linux 的 NPTL 实现中,pthread_t本质是进程地址空间中的一个地址 (指向线程控制块TCB的指针),线程库通过该地址访问线程的私有数据(如栈、寄存器上下文等)。

4.2 线程 ID 与进程地址空间的关系

进程地址空间为线程提供了共享与隔离的基础,线程 ID 的存储和使用与地址空间布局紧密相关:

进程地址空间布局

线程私有数据在地址空间中的位置

  • 主线程:栈位于进程地址空间的传统栈区(低地址附近),与进程的栈共享同一片区域。
  • 其他线程 :栈由 pthread 库在共享库区域与堆之间动态分配(独立于主线程栈),保证线程栈的私有性。
  • 线程控制块(TCB) :存储线程 ID(pthread_t指向的地址)、寄存器上下文、信号屏蔽字等私有信息,通常位于线程栈的顶部或单独的内存区域,属于进程地址空间的共享区(线程库管理)。

4.3 为何pthread_t是地址?

Linux 的 NPTL 线程库通过 "地址即标识" 的设计简化线程管理:

  • pthread_t直接指向线程的 TCB(线程控制块),线程库可通过该地址快速访问线程的私有数据(如栈指针、状态标志)。
  • 无需额外的全局表维护 ID 与线程的映射关系,减少了操作开销(如pthread_join可直接通过pthread_t找到线程的退出状态)。
  • 进程地址空间的独立性保证了pthread_t在进程内的唯一性(不同进程的地址空间隔离,同一地址在不同进程中对应不同线程)。

总结

  • 内核线程 ID(LWP):系统级唯一标识,用于内核调度,全局可见。
  • 用户态线程 ID(pthread_t):进程内唯一标识,本质是地址(指向 TCB),用于线程库操作。
  • 地址空间角色 :线程共享进程的代码段、数据段、堆等,但其栈和 TCB(含pthread_t)位于私有区域,保证了执行独立性。

理解两种线程 ID 的区别,是掌握多线程调试(如gdb查看线程)和线程库原理的关键。

5. 线程封装

Thread.hpp

cpp 复制代码
#ifndef THREAD_HPP
#define THREAD_HPP

#include <iostream>
#include <string>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <functional>

namespace ThreadModlue
{
    static uint32_t number = 1;

    class Thread
    {
        using func_t = std::function<void()>;

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

        void EnableRunning()
        {
            _isrunning = true;
        }

        static void *Routine(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            self->EnableDetach();
            if (self->_isdetach)
                self->Detach();

            pthread_setname_np(self->_tid, self->_name.c_str());
            self->_func();

            return nullptr;
        }

    public:
        Thread(func_t func)
            : _tid(0),
              _isdetach(false),
              _isrunning(false),
              res(nullptr),
              _func(func)
        {
            _name = "Thread-" + std::to_string(number++);
        }

        void Detach()
        {
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            EnableDetach();
        }

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

        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    std::cerr << "Failed to cancel thread: " << _name << std::endl;
                    return false;
                }
                else
                {
                    _isrunning = false;
                    std::cout << _name << " stopped" << std::endl;
                }
            }
            return true;
        }

        void Join()
        {
            if (_isdetach)
                return;
            int n = pthread_join(_tid, &res);
            if (n != 0)
            {
                std::cerr << "Failed to join thread: " << _name << std::endl;
            }
            else
            {
                std::cout << "Join successful" << std::endl;
            }
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        std::string _name;
        bool _isdetach;
        bool _isrunning;
        void *res;
        func_t _func;
    };
}

#endif

Main.cc

cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>
#include <vector>

using namespace ThreadModlue;

int main()
{
    std::vector<Thread> threads;
    for (int i = 0; i < 10; i++)
    {
        threads.emplace_back([]()
                             {
        while(true)
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            std::cout << "我是一个新线程: " << name << std::endl; 
            sleep(1);
        } });
    }
    for (auto &thread : threads)
    {
        thread.Start();
    }

    for (auto &thread : threads)
    {
        thread.Join();
    }

    return 0;
}
相关推荐
Java 码农5 小时前
CentOS 7 上安装 PostgreSQL
linux·postgresql·centos
筑梦之路5 小时前
CentOS 7 升级perl版本到5.40.3 —— 筑梦之路
linux·运维·centos
一个不秃头的 程序员5 小时前
从 0 到上线、长期运行、后续更新的**全流程**(适配 CentOS 服务器)
linux·服务器·centos
努力学习的小廉6 小时前
深入了解linux网络—— 自定义协议(上)
linux·服务器·网络
bcgbsh7 小时前
Linux开机启动脚本(cron 的 @reboot 特性)
linux·cron
听风吹雨yu7 小时前
RK3588从数据集到训练到部署YoloV8
linux·yolo·开源·rk3588·rknn
iconball8 小时前
个人用云计算学习笔记 --19 (MariaDB服务器)
linux·运维·笔记·学习·云计算
Lynnxiaowen8 小时前
今天我们开始学习python3编程之python基础
linux·运维·python·学习
Chandler248 小时前
一图掌握 操作系统 核心要点
linux·windows·后端·系统