c的多线程怎么实现原理 pthread原理与加锁示例
时间:2026-08-10 | 作者:宇宙开黑者 | 阅读:0C语言标准本身没有统一的多线程语法,常见实现路径主要有两类:类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_lock、pthread_mutex_unlock、pthread_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接口的写法与原理就能真正对应起来。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- c 语言基础教程怎么入门?语法、流程与示例说明
- 时间:2026-08-13
-
- c实现多线程 pthread创建回收与互斥锁同步
- 时间:2026-08-10
-
- c++11 正则表达式 std::regex写法与异常处理
- 时间:2026-08-10
-
- 多线程c语言实现 pthread创建同步与完整示例
- 时间:2026-08-10
-
- vc 正则表达式 std::regex与Boost.Regex选择
- 时间:2026-08-10
-
- c++ 正则表达 regex匹配提取替换示例
- 时间:2026-08-10
-
- 正则表达式c语言
- 时间:2026-08-10
-
- c多线程的实现方式 | 常用方案与选择
- 时间:2026-08-10
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15