当前位置: 首页 > news >正文

C 进阶(10) - 线程

在 Linux C 编程中多线程是实现高并发、提升程序性能的核心技术。Linux 下的多线程编程遵循POSIX 线程pthread标准。你可以把线程理解为“轻量级的进程”。同一个进程内的多个线程共享进程的内存、文件描述符等资源但每个线程又有自己独立的执行流和栈空间。线程概念如果把一个进程比作一套房子那么线程就是住在房子里的人共享资源住在同一套房子里的人共享客厅、厨房、卫生间对应进程中的代码段、全局变量、文件描述符等共享资源。独立活动每个人都有自己的房间和私人物品对应线程独立的栈空间、寄存器和程序计数器可以各自做不同的事情独立执行代码。在 Linux 内核中线程和进程并没有本质的区别它们都被统称为“任务task”用task_struct结构体来描述。线程之所以被称为“轻量级进程LWP”是因为它共享了进程的大部分资源创建和切换的开销远小于传统进程。 进程与线程的核心区别结合之前的进程这里做一个直观的对比对比项进程 (Process)线程 (Thread)资源拥有拥有独立的地址空间和系统资源共享进程的内存、文件等资源开销创建、切换、销毁开销大轻量级开销极小通信方式需要 IPC管道、共享内存等直接读写全局变量通信极快稳定性进程间互不影响隔离性好一个线程崩溃会导致整个进程崩溃基本单位资源分配的基本单位CPU 调度的基本单位线程标识提到“线程标识”其实存在两套完全不同的 ID 体系。很多初学者容易把它们搞混理解它们的区别对于调试和高级编程非常重要。这两套体系分别是用户级线程 ID (pthread_t)和内核级线程 ID (LWP)。 用户级线程 ID (pthread_t)这是我们平时使用 POSIX 线程库pthread时最常打交道的 ID。获取方式通过pthread_self()函数获取。本质与作用域它是由线程库如 Linux 下的 NPTL在用户空间维护的标识符。它的作用域仅限于当前进程内部用来在进程内唯一区分不同的线程。数据类型pthread_t是一个不透明的数据类型。在 Linux (glibc) 中它通常是一个无符号长整型unsigned long但在其他系统如 FreeBSD上可能是一个指针。因此绝对不能直接用来比较两个pthread_t而应该使用pthread_equal()函数。打印方式在 Linux 下通常可以强转为unsigned long后用%lu打印。代码示例#include pthread.h #include stdio.h void* thread_func(void* arg) { // 获取当前线程的用户级 ID pthread_t tid pthread_self(); printf(子线程用户级线程ID %lu\n, (unsigned long)tid); return NULL; } int main() { pthread_t tid; pthread_create(tid, NULL, thread_func, NULL); printf(主线程子线程用户级ID %lu\n, (unsigned long)tid); pthread_join(tid, NULL); return 0; }⚙️ 内核级线程 ID (LWP)这是 Linux 内核真正用来调度和管理线程时使用的 ID。获取方式通过gettid()系统调用获取在 C 语言中通常写作syscall(SYS_gettid)。本质与作用域在 Linux 内核中线程本质上就是“轻量级进程LWP, Light Weight Process”。这个 ID 是系统全局唯一的内核调度器就是靠它来识别和调度线程的。查看方式你在终端使用top命令按H键切换线程视图或ps -aL命令时看到的 PID/TID 列就是这个内核级线程 ID。特点主线程的 LWP 等于整个进程的 PID而子线程的 LWP 则是独立分配的全局唯一值。代码示例#include stdio.h #include sys/syscall.h // 需要包含此头文件 #include unistd.h int main() { // 通过系统调用获取内核级线程ID (LWP) pid_t lwp syscall(SYS_gettid); printf(当前线程的内核级ID (LWP)%d\n, lwp); printf(当前进程的PID%d\n, getpid()); return 0; } 核心区别对比为了让你更直观地理解这里有一份核心区别对比表特性用户级线程 ID (pthread_t)内核级线程 ID (LWP)获取函数pthread_self()syscall(SYS_gettid)/gettid()定义者POSIX 线程库 (用户态)Linux 内核 (内核态)作用域仅在当前进程内唯一系统全局唯一本质线程库维护的句柄/地址内核调度的轻量级进程 ID典型用途配合pthread_join等库函数管理线程系统级调试、设置 CPU 亲和性、top查看调用开销极低无系统调用较高需要陷入内核 总结与避坑日常开发如果你只是在代码里进行线程的创建、等待、取消等常规操作使用pthread_self()就足够了它符合 POSIX 标准跨平台且开销极小。系统调试与高级应用如果你需要排查系统级的性能问题比如用perf或top抓某个特定线程的 CPU 占用或者需要调用 Linux 特有的系统接口如设置线程的 CPU 亲和性sched_setaffinity这时就必须使用内核级的LWP ID。不要混淆千万不要把pthread_self()打印出来的数字拿到top或ps命令里去查那是查不到的。线程创建在 Linux C 语言中创建线程主要依赖于 POSIX 线程标准Pthreads核心函数是pthread_create。 核心函数pthread_create详解使用线程需要包含pthread.h头文件并且在编译时必须加上-pthread选项例如gcc main.c -o main -pthread。函数原型#include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);参数详解thread输出参数传入一个pthread_t类型的指针用于接收新创建线程的唯一标识符线程ID。attr输入参数用于设置线程的属性如栈大小、分离状态等。一般传NULL表示使用默认属性。start_routine输入参数线程的入口函数回调函数。该函数必须满足void* func(void*)的形式即接受一个void*参数并返回一个void*。arg输入参数传递给线程入口函数的参数。如果不需要传参填NULL即可。返回值成功返回0。失败返回非 0 的错误码如EAGAIN表示资源不足。注意pthread 函数出错时不会设置全局变量errno需要直接检查返回值。 基础创建示例下面是一个标准的线程创建与等待的完整示例#include stdio.h #include stdlib.h #include pthread.h #include string.h // 线程入口函数 void* thread_func(void* arg) { char* msg (char*)arg; printf(子线程正在执行收到的消息是: %s\n, msg); return NULL; // 线程正常退出 } int main() { pthread_t tid; // 定义线程ID char* input 你好我是主线程传进来的参数; int ret; // 创建线程 ret pthread_create(tid, NULL, thread_func, input); if (ret ! 0) { // pthread 函数返回的是错误码需要用 strerror 转换 fprintf(stderr, 创建线程失败: %s\n, strerror(ret)); exit(EXIT_FAILURE); } printf(主线程子线程创建成功等待其结束...\n); // 阻塞等待子线程结束回收资源 pthread_join(tid, NULL); printf(主线程子线程已退出程序结束。\n); return 0; }⚠️ 线程创建的常见避坑指南主线程提前退出导致子线程“夭折”线程共享同一个进程的地址空间。如果主线程main函数执行完毕直接return或调用了exit()整个进程会立刻终止此时即使子线程还没执行完也会被强制杀死。因此主线程通常需要使用pthread_join()来等待子线程执行完毕。传递局部变量的地址悬垂指针如果你给线程传递的参数是一个局部变量的地址例如int num 10; pthread_create(..., num);当主线程的该函数作用域结束局部变量会被销毁子线程拿到的就会是一个无效的悬垂指针极易导致程序崩溃。如果需要传递复杂数据建议使用堆内存malloc或全局变量。忘记链接 pthread 库在 Linux 下编译时如果只写gcc main.c会报链接错误。必须加上-pthread编译选项它不仅会链接线程库还会启用一些线程安全的宏定义。线程终止在 Linux C 多线程编程中线程的终止退出机制非常有讲究。根据终止的主体是线程自己走、被别人杀、还是主线程退出不同处理方式和对整个进程的影响也完全不同。我们可以把线程终止分为以下三种核心情况 线程主动退出优雅离场线程完成任务后可以通过以下三种方式主动结束自己。这三种方式都能让线程正常触发清理函数并安全地释放资源从线程入口函数中return返回这是最自然的方式函数的返回值就是线程的退出码。调用pthread_exit()函数线程可以在入口函数或它调用的任何子函数中调用此函数主动退出。被其他线程取消pthread_cancel虽然是被动触发但线程会在“取消点”响应请求并主动退出。⚠️ 绝对禁忌千万不要在线程里调用exit()exit()是用来终止整个进程的。如果某个子线程调用了exit()整个进程会立刻崩溃导致同进程内的所有其他线程包括主线程被强制杀死。 主线程退出对子线程的影响极易踩坑这是新手最容易搞混的地方主线程main函数退出了子线程会怎样这完全取决于主线程是怎么退出的情况一主线程直接return 0或调用exit()整个进程会立即终止所有子线程会被强制杀死无论它们是否还在运行。情况二主线程调用pthread_exit()主线程会“单独死亡”但进程不会终止。其他子线程会继续在后台运行直到所有线程都结束后进程才会真正退出。对比代码示例// 情况一主线程 return子线程还没打印就会被强制杀掉 int main() { pthread_t tid; pthread_create(tid, NULL, worker, NULL); return 0; // 进程立即终止子线程夭折 } // 情况二主线程 pthread_exit子线程能完整运行 int main() { pthread_t tid; pthread_create(tid, NULL, worker, NULL); pthread_exit(NULL); // 主线程退出但进程存活子线程继续跑 } 强制终止其他线程pthread_cancel一个线程可以请求强制取消另一个线程这就像给目标线程发送了一个“死亡通知”。函数原型int pthread_cancel(pthread_t thread);取消点Cancellation Pointspthread_cancel并不是魔法它不会让线程立刻原地消失。目标线程只有运行到取消点时才会真正响应取消请求并退出。常见的取消点包括sleep、read、write、printf等阻塞型系统调用或标准 I/O 操作。资源泄漏风险由于是强制终止如果目标线程正拿着互斥锁或者占用了堆内存突然被取消会导致死锁或内存泄漏。因此必须配合线程清理处理程序pthread_cleanup_push和pthread_cleanup_pop来确保资源被安全释放。♻️ 终止后的资源回收防止“僵尸线程”线程终止后它的内核资源如线程描述符、栈空间等并不会自动消失。如果不处理会产生类似“僵尸进程”的资源泄漏。回收方式有两种可结合状态Joinable默认状态必须由其他线程通常是主线程调用pthread_join()来等待它结束并“收尸”回收资源并获取退出状态。分离状态Detached调用pthread_detach()将线程设为分离状态。线程结束后内核会自动回收其资源不需要其他线程来等待。 线程终止方式核心对比为了让你更直观地掌握这里有一份核心对比表终止方式适用场景核心特点与注意事项return线程入口函数自然结束最常用返回值即为退出码pthread_exit()线程在任意深层子函数中退出仅终止当前线程不杀进程pthread_cancel()外部强制终止耗时/卡死的线程需在取消点响应注意防范资源泄漏exit()禁忌会直接杀死整个进程导致所有线程陪葬在日常开发中最推荐的优雅做法是让线程通过return或pthread_exit自然退出并在主线程中通过pthread_join进行等待和资源回收。线程同步在 Linux C 多线程编程中线程同步是保证程序正确性和稳定性的基石。由于同一进程内的多个线程共享全局变量、堆内存等资源当它们同时访问尤其是修改同一块共享数据时就会发生竞态条件Race Condition导致数据错乱、逻辑异常甚至程序崩溃。为了解决这些问题Linux 的 pthread 线程库提供了多种工业级的同步机制。下面为你详细拆解最核心的几种同步方式 互斥锁Mutex—— 最通用的“独占锁”互斥锁是线程同步中最基础、最常用的机制。它的核心思想非常直白同一时刻只允许一个线程持有锁并进入临界区操作共享资源的代码段。工作机制线程在访问共享资源前先尝试加锁。如果锁空闲则加锁成功并继续执行如果锁已被其他线程占用当前线程会阻塞休眠不占用 CPU直到锁被释放。核心 APIpthread_mutex_init(mutex, NULL)初始化互斥锁。pthread_mutex_lock(mutex)加锁拿不到锁会一直阻塞等待。pthread_mutex_unlock(mutex)解锁释放锁唤醒等待的线程。pthread_mutex_destroy(mutex)销毁锁。适用场景任何需要保护共享数据不被并发修改的场景如全局计数器、共享队列等。 读写锁Read-Write Lock—— “读多写少”的性能优化互斥锁虽然安全但有时候过于严格即使是多个线程同时读取数据互斥锁也会强制它们排队。读写锁正是为了解决这种读多写少的场景而生的。核心规则读锁共享允许多个线程同时持有读锁并发地读取共享资源。写锁独占当有线程持有写锁时其他任何线程无论是读还是写都不能加锁反之当有线程持有读锁时写锁请求也会被阻塞。核心 APIpthread_rwlock_rdlock(rwlock)加读锁。pthread_rwlock_wrlock(rwlock)加写锁。pthread_rwlock_unlock(rwlock)解锁。适用场景频繁读取、偶尔修改的共享数据如配置信息表、路由表等。⏳ 条件变量Condition Variable—— 线程间的“等待与通知”互斥锁解决了“抢资源”的问题但无法解决“等条件”的问题。条件变量通常必须配合互斥锁一起使用用于让线程在某个条件不满足时主动挂起等待直到其他线程改变了条件并通知它。核心机制等待线程调用pthread_cond_wait()时会先自动释放互斥锁然后进入休眠状态。通知其他线程在修改了条件后调用pthread_cond_signal()唤醒一个等待的线程或pthread_cond_broadcast()唤醒所有。被唤醒的线程会重新尝试获取互斥锁然后继续执行。适用场景经典的生产者-消费者模型消费者等待缓冲区有数据生产者等待缓冲区有空位。 信号量Semaphore—— 资源计数的“通行证”信号量本质上是一个非负整数计数器用于控制同时访问特定资源的线程数量。核心操作P操作 (sem_wait)申请资源将信号量值减 1。如果值为 0线程阻塞等待。V操作 (sem_post)释放资源将信号量值加 1并唤醒等待的线程。适用场景控制对固定数量资源池的访问如数据库连接池、固定大小的环形缓冲区。⚡ 自旋锁Spinlock—— 极短临界区的“忙等待”自旋锁与互斥锁最大的区别在于拿不到锁时线程不会休眠而是原地循环自旋不断尝试获取锁。优缺点避免了线程上下文切换休眠和唤醒的开销但如果锁被占用的时间较长会白白浪费大量的 CPU 资源。适用场景锁持有的时间极短如只修改几个寄存器或极小的内存区域且不希望发生线程切换的场景。 核心同步机制对比总结表格同步机制核心特点适用场景互斥锁 (Mutex)独占访问拿不到锁会休眠保护临界区最通用的同步方式读写锁 (RWLock)读锁共享写锁独占读多写少的数据访问场景条件变量 (Cond)配合互斥锁实现等待/通知生产者-消费者、任务队列等信号量 (Semaphore)基于计数器的资源控制限制同时访问资源的线程数量自旋锁 (Spinlock)拿不到锁时原地忙等待临界区极短追求极致低延迟在实际的 Linux C 开发中互斥锁和条件变量的组合使用频率最高。掌握这些同步机制是写出高性能、无 Bug 的多线程程序的必修课。
http://www.gsyq.cn/news/1339812.html

相关文章:

  • 2026 一体化泵站厂家实力排行 本土优品多场景实用选型指南 - 资讯速览
  • RabbitMQ(七大模式+微服务+自用)
  • WorkBuddy(腾讯龙虾)开发 Minifilter文件系统过滤驱动
  • 大型语言模型走向专业化:多领域新型模型助力专业人士,成本效率双提升!
  • 如何在Docker容器中高效运行Android模拟器:完整实践指南
  • GetQzonehistory技术解析:构建高效的QQ空间历史数据备份系统
  • 洛雪音乐六音音源修复完整指南:快速恢复音乐播放功能
  • SGLang 多 GPU 分布式推理:张量并行与流水线并行的工程实践
  • Honey Select 2终极增强补丁:一站式游戏体验优化方案
  • ZeroOmega:浏览器代理切换的终极解决方案
  • 工业级知识图谱构建实践:建模、抽取、管理、计算、应用、演化六步法
  • 如何告别模组管理噩梦:XXMI启动器的3个革命性解决方案
  • 免费备份QQ空间历史记录的完整指南:5分钟永久保存你的青春记忆
  • 书匠策AI:拆解毕业论文的“全链路外挂“——一个教育博主的硬核科普
  • FineBI组件制作-表格
  • 书匠策AI降重降AIGC到底有多野?论文党看完直接封神!
  • 【ElevenLabs芬兰文语音实战指南】:2024最新API调用+音色微调+本地化合规避坑全攻略
  • 第八篇:《软件测试的经济学:投入与回报》
  • 【仅限VIP订阅者解锁】:Midjourney毛玻璃效果私有LoRA微调包+12组经生产验证的prompt模板(含Figma交付规范)
  • 2026年京东云OpenClaw/Hermes Agent配置Token Plan环境搭建指南
  • Windows与Office激活终极指南:KMS_VL_ALL_AIO完整解决方案
  • 铁路机车再生制动能量智能利用系统与关键技术【附程序】
  • TMS320VC5502PGF300:TI TMS320C55x系列定点DSP,300MHz,176-LQFP封装
  • 如何在Photoshop中构建AI原生工作流:SD-PPP的技术架构解析
  • 七、Linux系统下的文件IO (一)
  • UE5-MCP:模块化代码流水线与AI驱动的开发提效方案
  • 本源投影内生智能:从概率拟合到硅基生命的底层重构
  • Red Hat Enterprise Linux 10.2 和 9.8 发布,命令行 AI 辅助增强,多工具集性能升级
  • iFakeLocation完全指南:如何在3分钟内实现iOS设备虚拟定位
  • 面试 AI Agent 工程师会被问什么?40+ 真题 + 知识图谱全梳理