【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;
}
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言