冷静果断专注头像

Linux 如何创建线程代码实现与线程前置知识

线程前置知识(从零开始,大白话 + pthread 全套函数,适配你现在学的 Linux C++)

一、线程到底是什么

先回忆进程: 进程 = 操作系统分配资源的最小单位(独立内存、文件描述符、信号、堆),一个进程就是一个独立程序。

线程:进程内部执行的最小调度单位,又叫轻量级进程 LWP。

  • 一个进程里面,可以开好多个线程;所有线程共享这个进程的绝大部分资源
  • 每个线程自己私有的东西:自己的栈、寄存器、线程 ID (pthread_t)、独立的信号屏蔽字
  • 共享:全局变量、堆、文件描述符、代码段

举例子: 进程 = 一间房子 线程 = 房子里面的好几个人 所有人共用房子的冰箱、电视(全局 / 堆内存);但是每个人有自己的笔记本(自己的栈)。

重点区分: ✅ 进程之间资源隔离,通信必须 IPC(管道、共享内存) ✅ 同一个进程的线程天然共享数据,直接读全局变量就能通信,但是会抢资源,需要锁

一句话考点: 进程:资源分配单位;线程:CPU 调度单位。

LWP 全称:Lightweight Process 中文:轻量级进程

  • PCB:Process Control Block 进程控制块
  • TCB:Thread Control Block 线程控制块

二、pthread 头文件 + 编译注意

头文件:#include <pthread.h> 编译命令必须加 -lpthread(链接 POSIX 线程库)

g++ test.cpp -o test -lpthread
./test

C++11 std::thread 不用加这个,库封装好了。

-lpthread 是什么

先拆开:

  • l 是 link(链接)的简写
  • pthread 就是 POSIX thread 库

一句话:告诉编译器,编译的时候去链接 pthread 线程库,里面放着 pthread_create、pthread_join 这些函数的实现代码。

简单理解

头文件 #include <pthread.h> 只干一件事: 给你看函数声明、类型定义(比如 pthread_t),相当于给你看说明书,没有函数本体。

真正的函数代码(实现),放在一个独立的库文件 libpthread.so 里。 编译时不加 -lpthread:编译器认识这些函数名字,但是找不到函数本体 → 链接报错(undefined reference to pthread_create)。

gcc/g++ 的规则: -lxxx 代表链接 libxxx.so 动态库。 -lpthread → 链接 libpthread.so

顺序小提醒:-lpthread 一般放源码文件后面,放前面有时候会链接失败。

补充考点

  1. 动态库 libxxx.so:程序运行的时候才加载;静态库 libxxx.a:编译时打包进程序。
  2. POSIX:一套操作系统标准,pthread 就是 POSIX 标准规定的线程接口。

三、pthread 核心函数(一共 6 个,你现在学的全部都在这里)

1. pthread_create —— 创建线程(最重要)

函数原型:

int pthread_create(
    pthread_t *thread,          // 输出:线程ID,创建成功后填到这个变量
    const pthread_attr_t *attr, // 线程属性,一般填 nullptr 默认属性
    void *(*start_routine)(void *), // 线程入口函数(必须是这个格式!)
    void *arg                   // 传给入口函数的参数
);

返回值:成功返回 0;失败返回错误码(非 0)

⚠️ 入口函数固定格式,不能改:

void *函数名(void *args)
{
    // 线程跑的代码
    return nullptr; // 返回void*,最后交给pthread_join接收
}

为什么是 void_?为了通用:可以传任意类型指针(字符串、自定义对象 Request_)

示例:

pthread_t tid;
pthread_create(&tid, nullptr, sumCount, rq);

2. pthread_self —— 获取当前线程自己的 tid

原型:

pthread_t pthread_self(void);

作用:在线程函数内部,拿到自己的线程 ID 类比:getpid() 获取进程 pid

pthread_t me = pthread_self();

3. pthread_join —— 等待线程,回收资源

原型:

int pthread_join(pthread_t thread, void **retval);
  • thread:要等待哪个线程
  • retval:输出参数,用来接收线程 return 回来的 void * 返回值;不需要结果填 nullptr

特点:阻塞函数! 主线程走到这里,卡住不动,等到目标线程执行完毕,才继续往下跑。 两个作用:

  1. 同步:控制顺序,等子线程做完
  2. 回收线程内核资源(TCB),不 join 会产生僵尸线程
void *ret;
pthread_join(tid, &ret);

传地址(&ret)能干啥

把 ret 这个变量的地址传给 pthread_join: 函数内部可以通过这个地址,修改 main 里 ret 变量的值,把子线程 return 出来的指针写到 ret 里面。 伪代码示意 pthread_join 内部逻辑:

// 内部简化理解
if(retval != NULL)
{
    *retval = 子线程返回的那个void*;
}

*retval 就是修改外面的 ret。

类比: 想让函数修改一个 int 变量:

void func(int *p) { *p = 100; }
int a;
func(&a);

和这个逻辑一模一样! 只不过这里要修改的变量类型是 void*,所以参数变成 void**。

4. pthread_detach —— 分离线程(和 join 二选一)

原型:

int pthread_detach(pthread_t thread);

作用:告诉操作系统:这个线程退出后,自动回收资源,不需要别人 join 它

  • detach 之后,不能再调用 pthread_join,会报错
  • 适合:不需要等待、不需要拿返回结果的后台线程

两种写法:

// 创建之后主线程分离
pthread_detach(tid);
// 或者线程内部自己分离
pthread_detach(pthread_self());

重点:一个线程,要么 join,要么 detach,不能两个都干,也不能都不干(资源泄漏)

5. pthread_equal —— 比较两个线程 ID

原型:
pthread_t 是线程 ID 类型,它只是一个类型别名(typedef),POSIX 标准没有强制规定它必须是整数! 标准只要求:它能唯一标识一个线程,底层实现交给各个操作系统自己定。

int pthread_equal(pthread_t t1, pthread_t t2);

返回非 0 代表同一个线程;返回 0 代表不同线程。 ❌ 不要直接 if(t1 == t2) 跨平台比较!pthread_t 不一定是数字,可能是结构体。

if(pthread_equal(t1, t2))
{
    cout << "同一个线程" << endl;
}

6. pthread_exit —— 线程主动退出

原型:

void pthread_exit(void *retval);

作用:线程主动结束,retval 就是返回值,交给 pthread_join 拿

注意:在线程函数里面 return 和 pthread_exit 效果差不多; 但是!主线程 return,整个进程直接结束,所有子线程全部被杀掉! 主线程想退出、保留子线程跑:主线程用 pthread_exit (nullptr),不要 return。

pthread_exit((void*)100);

四、线程生命周期(背诵)

  1. 创建:pthread_create → 线程进入就绪态,等待 CPU 调度
  2. 运行:拿到 CPU,执行入口函数代码
  3. 阻塞:sleep、usleep、pthread_join、锁等待,让出 CPU
  4. 退出:return /pthread_exit
  5. 等待回收:
    • 没 detach:停在这里,等别人 join 回收(僵尸线程)
    • 已经 detach:退出后系统自动清理

五、两种线程模式对比(必考)

  1. 可结合线程(默认)
    • 需要 pthread_join 回收
    • 可以拿到返回值
    • 主线程可以等待它
  2. 分离线程(detach)
    • 退出自动回收,不用 join
    • 拿不到返回值
    • 后台任务常用

六、最容易踩的 4 个大坑(前期必记)

坑 1:主线程 return,整个进程销毁,所有子线程直接终止
坑 2:栈对象传指针给子线程:主线程栈销毁 → 野指针崩溃(所以跨线程传对象用 new 堆)
坑 3:忘记 join 也不 detach → 线程资源泄漏 坑 4:pthread_t 不能直接用 % d 打印,要用 % p 转 16 进制

七、最简完整示例(把上面函数串起来)

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;

// 功能:把pthread_t类型的tid转成十六进制字符串,规范打印tid
string toHex(pthread_t tid)
{
    char buf[64];
    // %p 是指针格式,强转void*,写法更标准
    snprintf(buf, sizeof(buf), "%p", (void*)tid);
    return buf;
}

void *threadFunc(void *args)
{
    // 调用toHex打印tid
    cout << "子线程tid: " << toHex(pthread_self()) << endl;
    cout << "收到参数: " << (char*)args << endl;
    sleep(2);
    return (void*)666; // 返回结果(实验取巧写法,64位linux可用)
}

int main()
{
    pthread_t tid;
    // 创建线程,传字符串参数
    pthread_create(&tid, nullptr, threadFunc, (void*)"hello thread");
    cout << "主线程创建完成,子线程tid: " << toHex(tid) << endl;

    void *ret;
    // 等待子线程结束,接收返回值
    pthread_join(tid, &ret);
    cout << "子线程返回值: " << (long long)ret << endl;

    return 0;
}

pid_t:整型,全局唯一,直接 == ,% d 打印。
pthread_t:抽象类型,进程内唯一,用 pthread_equal 比较,无标准打印格式,Linux 用 % lu/% p。

hex = hexadecimal,中文:十六进制

  • dec = decimal 十进制(我们平时用的 0,1,2…9)
  • hex = hexadecimal 十六进制(09,af,0x开头,比如 0x7f2381528700)
  • bin = binary 二进制
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;

// Request类:任务包,主线程传给子线程的参数
class Request
{
public:
    // 构造函数:初始化起始值、结束值、线程名字
    Request(int start, int end, const string &threadname)
        : start_(start), end_(end), threadname_(threadname)
    {
    }

public:
    int start_;       // 累加起始数字
    int end_;         // 累加结束数字
    string threadname_;// 线程名称,方便打印观察
};

// Response类:结果包,子线程计算完成后返回给主线程
class Response
{
public:
    // 构造函数:初始化结果、状态码
    Response(int result, int exitcode)
        : result_(result), exitcode_(exitcode)
    {
    }

public:
    int result_;      // 存放累加的总和结果
    int exitcode_;    // 状态码,这里0代表计算正常
};

// 线程入口函数,pthread规定固定格式:void* 函数名(void* args)
// args:主线程传进来的参数,这里是Request对象的地址
void *sumCount(void *args)
{
    // 1. 把通用void*指针,强转为Request*,拿到任务信息
    // static_cast 是C++推荐的安全转换,替代C风格 (Request*)args
    Request *rq = static_cast<Request*>(args);

    // 2. 在堆上新建Response对象,用来保存计算结果
    // 必须堆上new,因为这个对象要返回给主线程,函数结束不能被销毁
    Response *rsp = new Response(0, 0);

    // 3. 循环从start_累加到end_
    for(int i = rq->start_; i <= rq->end_; i++)
    {
        cout << rq->threadname_ << " is runing, caling..., " << i << endl;
        rsp->result_ += i;
        usleep(100000); // 休眠0.1秒,放慢速度,方便看打印过程
    }

    // 4. Request是主线程new出来的,子线程用完,在这里释放,防止内存泄漏
    delete rq;

    // 5. 返回Response指针,自动转成void*,交给pthread_join接收
    return rsp;
}

int main()
{
    pthread_t tid;  // 保存子线程ID

    // 在堆上创建任务对象:任务是1~100累加,线程名字叫thread 1
    Request *rq = new Request(1, 100, "thread 1");

    // 创建子线程
    // 参数1:tid的地址,用来输出线程ID
    // 参数2:线程属性,nullptr使用默认属性
    // 参数3:线程入口函数名字
    // 参数4:传给入口函数的参数(Request对象的地址)
    pthread_create(&tid, nullptr, sumCount, rq);

    void *ret;  // 用来接收子线程return返回的void*
    // 阻塞等待子线程执行结束,子线程返回值存入ret
    pthread_join(tid, &ret);

    // 把返回的void*强转为Response*,拿到结果包
    Response *rsp = static_cast<Response *>(ret);

    // 打印计算结果和状态码
    cout << "rsp->result: " << rsp->result_ << ", exitcode: " << rsp->exitcode_ << endl;

    // Response是子线程new出来的,主线程用完负责释放,防止内存泄漏
    delete rsp;

    return 0;
}

编译命令

g++ test.cpp -o test -lpthread
./test

重点回顾(对应代码)

  1. Request:主线程new → 传给子线程 → 子线程delete
  2. Response:子线程new → return给主线程 → 主线程delete
  3. 为什么两个对象都new在堆:控制生命周期,跨线程传递时不会提前销毁变成野指针
  4. pthread_join(tid, &ret):传ret的地址,因为函数需要修改ret这个局部变量

要不要我再给你出一个小修改练习:改成两个线程分别算150、51100,最后主线程汇总总和?

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2602_96210063/article/details/170743672

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--