位置:首页 > C > c的多线程怎么实现原理 pthread原理与加锁示例

c的多线程怎么实现原理 pthread原理与加锁示例

时间:2026-08-10  |  作者:宇宙开黑者  |  阅读:0

C语言标准本身没有统一的多线程语法,常见实现路径主要有两类:类Unix系统里常用POSIX线程库pthread,Windows里通常使用Windows线程API,因此多线程并不是C标准库自带能力。本文只讲类Unix系统下的pthread实现方式,重点回答C语言使用pthread怎么真正写出可运行程序,以及pthread接口背后的基本工作原理。

多线程的实现基础

进程是操作系统分配资源的基本单位,线程则是进程内部可以独立调度的执行流。同一进程中的多个线程共享同一份虚拟地址空间,因此代码段、全局变量、静态存储区和堆内存都可以共同访问;但每个线程仍然有自己的栈、程序计数器、寄存器上下文和线程标识,所以函数调用现场、局部变量和执行位置彼此独立。以pthread为例,程序调用pthread_create后,线程库会把线程入口函数、参数、栈等信息交给底层系统创建新线程,新线程建立完成后会从指定入口开始执行,调度器再根据系统策略安排它何时运行。不同操作系统和pthread实现细节并不完全相同,但共享地址空间、独立栈与独立执行流这几个核心特征是一致的。

  • 共享同一进程的虚拟地址空间
  • 每个线程拥有独立栈和寄存器上下文
  • pthread属于POSIX线程库而不是C标准库
  • 具体创建与调度细节会因操作系统和线程库实现而异

使用pthread创建线程并运行完整示例

如果目标平台是类Unix系统,可以直接使用pthread写出可编译运行的多线程程序。线程函数签名必须符合void *(*start_routine)(void *)这一接口要求,pthread_create的四个参数分别是线程ID输出地址、线程属性、线程入口函数和传入参数。下面这个示例创建两个线程,分别接收不同参数并返回结果,主线程再用pthread_join回收它们。

#include 
#include 
#include 
#include 

typedef struct {
    int id;
    const char *name;
    int repeat;
} ThreadArg;

void *worker(void *arg) {
    ThreadArg *data = (ThreadArg *)arg;
    int *result = malloc(sizeof(int));
    if (result == NULL) {
        perror("malloc");
        pthread_exit(NULL);
    }

    printf("thread %d (%s) startn", data->id, data->name);
    *result = data->repeat * 10;

    for (int i = 0; i < data->repeat; i++) {
        printf("thread %d running: %dn", data->id, i + 1);
    }

    printf("thread %d (%s) endn", data->id, data->name);
    return result;
}

int main(void) {
    pthread_t tid1, tid2;
    ThreadArg arg1 = {1, "alpha", 3};
    ThreadArg arg2 = {2, "beta", 5};
    void *ret1 = NULL;
    void *ret2 = NULL;
    int rc;

    rc = pthread_create(&tid1, NULL, worker, &arg1);
    if (rc != 0) {
        fprintf(stderr, "pthread_create tid1 failed: %sn", strerror(rc));
        return 1;
    }

    rc = pthread_create(&tid2, NULL, worker, &arg2);
    if (rc != 0) {
        fprintf(stderr, "pthread_create tid2 failed: %sn", strerror(rc));
        return 1;
    }

    rc = pthread_join(tid1, &ret1);
    if (rc != 0) {
        fprintf(stderr, "pthread_join tid1 failed: %sn", strerror(rc));
        return 1;
    }

    rc = pthread_join(tid2, &ret2);
    if (rc != 0) {
        fprintf(stderr, "pthread_join tid2 failed: %sn", strerror(rc));
        free(ret1);
        return 1;
    }

    if (ret1 != NULL) {
        printf("thread 1 result = %dn", *(int *)ret1);
        free(ret1);
    }
    if (ret2 != NULL) {
        printf("thread 2 result = %dn", *(int *)ret2);
        free(ret2);
    }

    return 0;
}

编译方式可以直接写成cc demo.c -pthread -o demo,运行使用./demo。这里有几个关键点必须注意:一是传给线程的参数对象在整个线程执行期间都必须有效,所以上例把arg1和arg2放在main函数作用域内并等到join完成后才结束;二是线程返回值通过pthread_join取回;三是pthread相关接口通常返回错误码而不是直接设置errno,所以示例里用strerror(rc)输出错误原因。

  • 线程函数签名必须匹配void *(*)(void *)
  • pthread_create依次接收线程ID、属性、入口函数和参数
  • 主线程用pthread_join等待并回收线程
  • 编译命令通常为cc 文件名.c -pthread -o 可执行文件

pthread接口背后的执行与调度原理

从原理上看,pthread_create并不是简单地调用一个普通函数,而是请求系统创建新的线程执行流。线程库会准备线程控制块、栈空间、启动参数和入口包装逻辑,然后由底层内核或线程实现机制把新线程加入可调度队列。调度器真正让它获得CPU后,新线程才会从pthread_create指定的入口函数开始执行。由于多个线程属于同一进程,它们天然共享同一地址空间,所以一个线程写入堆内存或全局变量,其他线程有机会读到这些内容;但每个线程又有独立栈和寄存器状态,因此各自函数里的局部变量、返回地址和当前执行位置互不覆盖。

线程切换时,系统需要保存当前线程的寄存器上下文,并恢复下一个线程的上下文,这就是上下文切换。切换本身有成本,因此线程数并不是越多越好。互斥锁的工作机制也可以这样理解:无竞争时,线程加锁后直接进入临界区,解锁后其他线程再尝试进入;有竞争时,后到线程会因为锁已被持有而等待,直到持锁线程解锁后,再由线程库和调度器唤醒并重新竞争。具体等待、唤醒和调度的实现细节会因Linux、BSD等系统以及不同pthread库而有差异,但对程序员来说,看到的统一接口仍是pthread_mutex_lockpthread_mutex_unlockpthread_join这些调用。

  • pthread_create会形成新的执行流而不是普通函数调用
  • 线程共享地址空间但拥有独立栈和寄存器上下文
  • 上下文切换需要保存和恢复执行现场
  • 锁在无竞争和有竞争时表现不同

共享数据与线程同步的竞态对照示例

只理解概念还不够,真正的难点是知道锁应该放在哪里。下面先看一个共享计数器的典型竞态:两个线程都对同一个全局counter做自增,如果没有同步保护,counter++往往会被拆成读取、修改、写回三个步骤,两个线程可能读到同一个旧值,最终导致结果小于预期。

未加锁时的核心代码可以概括为:

for (int i = 0; i < 100000; i++) {
    counter++;
}

这段代码看起来简单,但并不能保证两个线程累计后一定得到200000。修复方法是把真正访问共享变量的那一小段代码放进临界区,并按照初始化、加锁、解锁、销毁的完整顺序使用互斥锁:

#include 
#include 
#include 

static long counter = 0;
static pthread_mutex_t counter_lock;

void *count_worker(void *arg) {
    int loops = *(int *)arg;
    for (int i = 0; i < loops; i++) {
        pthread_mutex_lock(&counter_lock);
        counter++;
        pthread_mutex_unlock(&counter_lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    int loops = 100000;

    if (pthread_mutex_init(&counter_lock, NULL) != 0) {
        fprintf(stderr, "pthread_mutex_init failedn");
        return 1;
    }

    if (pthread_create(&t1, NULL, count_worker, &loops) != 0) {
        fprintf(stderr, "pthread_create t1 failedn");
        pthread_mutex_destroy(&counter_lock);
        return 1;
    }
    if (pthread_create(&t2, NULL, count_worker, &loops) != 0) {
        fprintf(stderr, "pthread_create t2 failedn");
        pthread_join(t1, NULL);
        pthread_mutex_destroy(&counter_lock);
        return 1;
    }

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("counter = %ldn", counter);
    pthread_mutex_destroy(&counter_lock);
    return 0;
}

这段修复后的代码说明了锁的放置原则:锁要包住共享状态被读取和改写的最小必要区域,而不是随意把整段业务逻辑都锁住。若进一步使用条件变量等待某个状态成立,条件检查必须放在循环里而不能只写一次if,因为线程被唤醒后,共享状态可能已经再次变化,或者出现伪唤醒,循环重新检查才能保证逻辑正确。

  • 未同步的counter++会产生竞态条件
  • 互斥锁应包住共享数据读改写的最小临界区
  • 使用顺序通常是init、lock、unlock、destroy
  • 条件变量等待条件时必须循环检查共享状态

从原理到可靠实现

设计pthread程序时,可以先把任务拆成相对独立的工作单元,再明确哪些数据只是线程私有,哪些数据必须共享。凡是共享数据,都要提前决定采用互斥锁、条件变量、读写锁还是原子操作;凡是传给线程的参数,都要保证在线程结束前一直有效。对于文章开头的完整示例,线程参数结构体由main函数持有到join结束,因此不会出现悬空指针;对于计数器示例,真正共享的是counter,所以锁直接围绕counter++放置。这样写出来的多线程程序不仅能跑起来,也更容易定位死锁、竞争和性能瓶颈。若需要跨平台支持,再分别封装pthread和Windows线程API,而不要误以为C语言本身已经规定了统一线程语法。

  • 先区分线程私有数据与共享数据
  • 让参数生命周期覆盖线程执行期
  • 同步机制要直接对应共享状态访问点
  • 跨平台时应分别封装不同系统线程实现路径

如果问题是C的多线程怎么实现,实践层面最常见的答案就是在类Unix系统中使用pthread:定义符合签名的线程函数,安全传参,调用pthread_create创建线程,使用pthread_join回收,再用互斥锁和条件变量保护共享状态。理解共享地址空间、独立栈、上下文切换和锁竞争这些底层过程后,pthread接口的写法与原理就能真正对应起来。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多