Linux系统:线程的概念和控制


前言

学习线程在编程中非常重要,因为它允许程序同时执行多个任务,从而提升效率和用户体验。比如,在Ubuntu 20.04上用C/C++写一个程序,可以用线程让一个任务下载文件,另一个任务显示进度条,避免程序卡顿。线程比进程更轻量,共享内存,节省资源,适合处理多任务场景。掌握线程能帮你理解Linux系统如何高效运行(如用htop查看线程),为学习并发编程、服务器开发、甚至云计算等高级知识打下基础。通过实践,比如用C/C++语言的pthread库写一个多线程程序,你能直观感受线程如何让程序更快、更灵活,为未来开发复杂应用奠定坚实基础。


一,线程有什么用

  • 当我们需要让 CPU 执行多个任务时,最直观的做法是用 fork 创建新进程,让不同进程并行执行。但进程的创建和切换代价较大,因为需要为新进程分配独立的地址空间和系统资源。

  • 为了解决这一问题,Linux提供了线程机制:线程是进程内部的执行单元,多个线程共享同一个进程的内存和资源,只需要为线程分配独立的栈和少量必要的数据。因此,线程的创建和切换比进程更轻量,同时还能方便地共享数据,更适合在同一应用程序内部并发执行多个任务。


二,线程是什么?

线程(thread)是一个程序里可以独立运行的“子任务”。想象一个程序像一个厨房,进程(program)是整个厨房,而线程是厨房里同时干活的厨师。每个厨师(线程)可以做不同的事,比如一个切菜,一个炒菜,但他们共享厨房的食材和工具(内存和资源)。线程让程序能同时做多件事,运行更快、更顺畅。

我们之前都学过进程就是PCBPCB在linux系统的定义中其实就是task_struct,而线程其实也是进程当中的task_stuct结构,如果这样理解我们会觉得很困惑,一个进程不是只能有一个PCB,那也应该只有一个task_stuct啊,为什么线程也是task_struct结构?

其实我们可以这样去理解,在进程中存在多个task_stuct,它们都共享一份资源和代码,它们分别在进程的不同代码段执行着属于它们的任务而它们都是线程单位,但是必须有一个主线程当作进程的代表==,这个线程则被称为PCB,如下图所示:
在这里插入图片描述
我们之前的理解,PCB就是task_struct,在这里有多个task_struct,但是只有一个task_struct是主线程,但是在CPU的眼中这几个task_struct都是PCB,处理方式都是一样的,只不过这些线程都共用一份资源。


2-1 线程的优点

  • 并发执行,提高程序响应速度:一个进程内可以有多个线程同时运行
  • 内存共享,数据交互方便:线程共享进程的全局变量和堆空间
  • 轻量级,开销比进程小:线程共享大部分进程资源(代码段、堆、全局变量),只有栈和寄存器独立
  • 更好利用多核 CPU:多线程程序可以把任务分配到不同 CPU 核心,实现真正并行

2-2 线程的缺点

  • 性能可能下降:想象一下,多线程就像一群员工在一个办公室里做任务,如果任务都是 计算密集型(每天都在算公式),而办公室里 桌子不够多(CPU 核心少),大家反而要排队轮流用桌子,这样不仅没加快工作效率,还会有额外的开销:每次换人要整理桌面(线程切换)、协调谁用资源(锁/同步),整体效率可能下降。
  • 稳定性容易出问题:多线程就像办公室里多人共享一台打印机,如果没好好排队或者抢资源,可能出现 冲突、打印错顺序,甚至机器卡住(竞态条件、死锁)
  • 访问控制有限:线程共享整个进程的内存和资源,就像办公室的所有人都能用同一个储物柜和打印机,线程里做错事(比如错误修改内存、关闭文件)可能 影响整个办公室,不仅是自己任务失败,相比之下,进程就像不同办公室,有门禁系统,互相不会轻易干扰
  • 编程难度更高:写多线程程序不仅要设计好策略,还要调试复杂的时间顺序问题,而且错误可能 偶尔出现、难以复现,比单线程程序难很多
  • 线程异常:当线程发生除零错误,或者野指针的时候,线程会崩溃,同时进程也会崩溃

2-3 共享和独占

  • 基本概念
    • 进程:操作系统分配资源的基本单位,每个进程都有自己的内存空间、代码和数据
    • 线程:进程里的执行单元,同一个进程里的线程共享大部分资源,但每个线程有自己独立的栈和寄存器
  • 共享资源
    • 代码段:大家看的程序代码都是一样的,不需要重复复制
    • 全局变量和堆:线程可以操作同一块数据,比如同一个共享缓存或数组
    • 文件描述符:多个线程可以同时操作同一个文件、socket 或管道
    • 信号处理器:线程共享信号处理方式,发送信号时整个进程内的线程都能响应
  • 独占资源
    • :每个线程的函数调用、局部变量和返回地址都放在自己的栈里,不会互相干扰
    • 寄存器和程序计数器:每个线程都有自己的 CPU 上下文,保证执行顺序独立
    • 线程 ID:每个线程都有唯一 ID,方便调度和管理

三,线程控制

3-1 POSIX线程库

POSIX是一组 IEEE 制定的标准,目标是让不同的 Unix 系统(比如 Linux、BSD、Solaris)能提供统一的 API,这样程序员写的代码可以在不同系统上编译和运行,它定义了一套 C 语言函数 API,用于创建和管理线程。

POSIX 线程编程中,相关函数构成了一个完整的 API 系列,几乎所有函数名都以 pthread_ 作为前缀。
使用这些函数时,需要:

  • 在代码中包含头文件 <pthread.h>
  • 在编译时通过编译器选项 -lpthread 显式链接线程库

3-2 创建线程

创建线程要用到的函数是pthread_create,函数原型:

#include <pthread.h>

int pthread_create(
    pthread_t *thread,              // 输出:新线程的 ID
    const pthread_attr_t *attr,     // 线程属性,通常用 NULL 表示默认属性
    void *(*start_routine)(void *), // 新线程要执行的函数
    void *arg                       // 传给线程函数的参数
);

返回值:0:创建成功,非 0:失败,返回错误码(如 EAGAIN、EINVAL 等)

演示代码:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void* worker(void* arg)
{
    int id = *(int*)arg;
    printf("Hello from thread %d\n", id);
    return NULL;
}
int main()
{
    pthread_t tid;
    int arg=42;
    if(pthread_create(&tid,NULL,worker,&arg)!=0)
    {
        perror("pthread_create");
        return 1;
    }
    pthread_join(tid,NULL);
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
Hello from thread 42

这里我们创建了一个进程,并且将这个arg的地址传给了进程,进程接收到了之后打印arg的值。这里的pthread_join可以等待我们的线程结束,让进程一直停留在这个位置直到我们对应的线程结束。


3-3 线程ID

线程有四个主要的ID形式,它们分别是:

  1. PID (Process ID):进程号,由内核分配,全系统唯一,标识的是“整个进程”(包含所有线程),所有线程调用 getpid() 得到的值都是一样的。

类比:公司注册号。

  1. TID(Thread ID):线程号,由内核分配,全系统唯一,每个线程都有独立的 TID,主线程的 TID = PID;其他线程的 TID ≠ PID,可以用 syscall(SYS_gettid) 获取。

类比:公司员工工号。

  1. LWP (Light Weight Process):轻量级进程,Linux 内核对线程的实现方式,在内核里,每个线程都是一个 LWP,本质上就是一个 task_struct,每个 LWP 都有自己的 TID,在命令里:ps -Lf 或 /proc/[pid]/task/ 看到的 LWP 值,其实就是 TID

类比:员工本人(有工号)。

  1. pthread_t(pthread_self()):POSIX 线程库对线程的抽象标识符,类型是 pthread_t(实现相关,可能是整数、指针或结构体),pthread_self() 返回这个标识,用来区分不同线程,它不一定等于 TID,但 pthread 库内部能映射到 TID

类比:公司人事系统的员工编号(不一定等于工号,但在公司里唯一)

举个例子
假设进程 PID=3000,里面有 3 个线程:

  • 主线程:PID=3000, TID=3000, LWP=3000, pthread_t=0x7f8a12345600
  • 子线程1:PID=3000, TID=3001, LWP=3001, pthread_t=0x7f8a12345700
  • 子线程2:PID=3000, TID=3002, LWP=3002, pthread_t=0x7f8a12345800

3-4 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit

演示代码:

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
void* thread_func(void* arg)
{
    printf("Hello from thread!\n");
    return (void*)42;
}
int main()
{
    pthread_t tid;
    void* retval;
    pthread_create(&tid,NULL,thread_func,NULL);
    pthread_join(tid,&retval);
    printf("子进程结束,返回值 = %ld\n",(long)retval);
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
Hello from thread!
子进程结束,返回值 = 42

这里有个重点就是,我们进程给线程传递的是指针的地址,用retval接收返回值,然后再进程类型转换,打印信息。

  • 线程可以调用pthread_ exit终止自己

演示代码:

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

// 子线程函数
void* thread_func(void* arg) {
    printf("子线程开始运行...\n");
    sleep(1);
    printf("子线程准备调用 pthread_exit 退出\n");
    pthread_exit((void*)123);   // 线程退出并返回一个值
}

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

    // 创建线程
    pthread_create(&tid, NULL, thread_func, NULL);

    // 等待子线程结束,并获取返回值
    pthread_join(tid, &retval);

    printf("子线程退出,返回值 = %ld\n", (long)retval);
    return 0;
}

演示结果:

子线程开始运行...
子线程准备调用 pthread_exit 退出
子线程退出,返回值 = 123
  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程

演示代码:

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

// 被取消的线程函数
void* worker(void* arg) {
    printf("工作线程启动,进入循环...\n");
    while (1) {
        printf("工作线程仍在运行...\n");
        sleep(1);  // 给机会被取消
    }
    return NULL;
}

// 控制线程函数
void* controller(void* arg) {
    pthread_t tid = *(pthread_t*)arg;
    sleep(3); // 等待一会儿,让工作线程运行
    printf("控制线程:准备取消工作线程!\n");
    pthread_cancel(tid);  // 取消目标线程
    return NULL;
}

int main() {
    pthread_t worker_tid, controller_tid;

    // 创建工作线程
    pthread_create(&worker_tid, NULL, worker, NULL);

    // 创建控制线程
    pthread_create(&controller_tid, NULL, controller, &worker_tid);

    // 等待两个线程结束
    pthread_join(controller_tid, NULL);
    pthread_join(worker_tid, NULL);

    printf("主线程结束。\n");
    return 0;
}

演示结果:

工作线程启动,进入循环...
工作线程仍在运行...
工作线程仍在运行...
工作线程仍在运行...
控制线程:准备取消工作线程!
主线程结束。

3-5 线程等待

线程等待,指的是一个线程停下来,等另一个线程先执行完毕,再继续往下走。在 POSIX 线程库里,最常见的就是 pthread_join —— 主线程调用它来等子线程结束。

函数原型:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

参数解析:

  • pthread_t thread:要等待的线程 ID(由 pthread_create 返回),主线程通过它来指定“我要等哪个线程结束”。
  • void **retval:用来接收目标线程的返回值,如果你不关心返回值,可以传 NULL,如果关心,就传一个指针的地址(即二级指针),结束后这个指针会指向线程函数的返回值。

为什么需要线程等待?

  • 保证结果正确:比如子线程负责计算结果,主线程要用这个结果,如果主线程不等,可能在结果还没算好时就提前用了,导致错误。
  • 避免资源没释放:如果主线程结束了,整个进程也会退出,子线程可能还没跑完,就被强行中止,用等待可以确保子线程有机会把该做的事做完。

资源释放演示代码

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void* work(void* arg)
{
    char* buffer=(char*)malloc(100);
    printf("子线程:申请了内存...\n");
    sleep(2);
    free(buffer);
    printf("子线程:释放了内存...\n");
    return NULL;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,work,NULL);
    printf("主线程:不等待子线程结束...\n");
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
主线程:不等待子线程结束...

这里我们的主进程并没有等待,直接退出return 0,那么其它的线程都会退出,资源不能得到释放。


3-6 分离线程

  • 当线程结束后,会留下一些数据等待pthread_join来处理,pthread_join将一些有用的数据保存下来,不用的数据释放掉,
  • 分离线程,就是线程退出后 系统会自动释放它占用的内核资源(栈、线程控制块等),不需要其他线程去调用 pthread_join
  • 默认创建的线程是 joinable(可连接的),退出后资源必须手动回收
  • 通过 pthread_detach 或线程创建属性设置可以把线程设为 分离状态

演示代码:

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

void* worker(void* arg)
{
    printf("线程%ld开始运行\n",(long)pthread_self());
    sleep(2);
    printf("线程%ld结束运行\n",(long)pthread_self());
    return (void*)42;
}
int main()
{
    pthread_t tid1,tid2;
    void* retval1;
    pthread_create(&tid1,NULL,worker,NULL);
    printf("主线程:等待joinable 线程结束...\n");
    pthread_join(tid1,&retval1);
    printf("joinable 线程结束,返回值=%ld\n",(long)retval1);
    pthread_create(&tid2,NULL,worker,NULL);
    pthread_detach(tid2);
    printf("主线程:分离线程已经创建,不用joinable...\n");
    sleep(3);
    printf("主线程结束\n");
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day3$ ./exe
主线程:等待joinable 线程结束...
线程140587074098944开始运行
线程140587074098944结束运行
joinable 线程结束,返回值=42
主线程:分离线程已经创建,不用joinable...
线程140587074098944开始运行
线程140587074098944结束运行
主线程结束
  • 分离线程:不需要 pthread_join,线程结束后系统自动回收资源,不能再用 pthread_join 获取返回值,否则会报错
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值