恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Linux 线程同步:竞态条件与互斥锁
首页
资讯中心
/
Linux 线程同步:竞态条件与互斥锁
Linux 线程同步:竞态条件与互斥锁
发布时间:2026/10/3 12:47:17
Linux 线程同步竞态条件与互斥锁课程尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理依据2026-09-29 17:52 录音转写课程 PDF 第 142—146 页。本节边界教材第 142—145 页讲竞态条件第 146 页开始讲互斥锁录音继续演示了互斥锁 API 和加锁后的累加程序因此一并整理为完整学习闭环。1. 本节要解决的问题多个线程都能访问进程中的全局变量。如果它们同时修改同一份数据程序结果可能随运行时序变化。本节的完整思路是多个线程修改共享变量num 操作互相穿插结果不稳定确定临界区进入临界区前加锁同一时刻只有一个线程修改修改后立即解锁结果稳定学完本节需要真正理解三件事pthread_join()只能保证打印发生在线程结束之后不能消除线程之间的竞争num不是不可分割的一步互斥锁保护的是一段访问共享状态的代码也就是临界区。2. 什么是竞态条件竞态条件race condition指两个或更多执行单元并发访问共享状态最终结果取决于它们执行的精确先后顺序。典型条件是至少两个线程并发执行它们访问同一个共享对象至少有一个线程执行写操作访问之间没有正确的同步。例如两个线程共同执行number;预期是加两次但实际可能只增加一次。2.1number为什么会丢失更新从 C 代码看number只有一行从执行过程看它通常包含三个逻辑步骤读取 number → 在寄存器中加 1 → 写回 number假设初值是 10时刻线程 A线程 B内存中的number1读取 10102读取 10103计算 11104计算 11105写回 11116写回 1111两个线程都执行了一次加一结果却从 10 变成 11。2.2 更严格的 C 语言结论如果多个线程在没有同步的情况下读写同一个普通变量并且至少有一次是写操作就产生了data race数据竞争。在 C 语言内存模型中数据竞争会导致未定义行为。因此不能只把它理解为“偶尔少加几次”。编译器优化后可能出现更多不可预测的结果。正确做法是使用互斥锁、原子操作或重新设计数据共享方式。3. 教材中的 20000 线程累加实验教材第 143—145 页让 20000 个线程分别执行一次(*p)最后由主线程等待全部线程并打印结果。核心结构是#defineTHREAD_COUNT20000staticvoid*add_thread(void*arg){int*parg;(*p);returnNULL;}主线程先创建所有线程再逐个等待for(inti0;iTHREAD_COUNT;i){pthread_create(threads[i],NULL,add_thread,number);}for(inti0;iTHREAD_COUNT;i){pthread_join(threads[i],NULL);}3.1 为什么已经join结果仍可能出错pthread_join()只表达主线程等待目标线程结束它没有表达各工作线程必须按顺序修改 number竞争发生在工作线程执行number时。主线程最后再等待不能倒回去修复已经丢失的更新。3.2 这个实验还要注意什么一次创建 20000 个线程只是为了放大现象不是推荐的工程设计线程创建和栈空间都有成本系统可能因为资源不足而创建失败原教材示例没有检查pthread_create()返回值若创建失败后仍对相应数组元素调用pthread_join()程序行为就不可靠。实际程序通常创建少量长期运行的工作线程让每个线程处理多个任务。4. 避免竞态条件的两条路线教材第 145 页给出两种思路4.1 减少或取消共享写入每个线程只修改自己的数据最后由一个线程统一合并。例如让每个线程先计算局部计数线程 A → local_a 线程 B → local_b 主线程 → total local_a local_b这是从程序结构上减少竞争通常也有更好的并行性能。4.2 给共享资源加锁如果多个线程必须修改同一个共享状态就在访问前取得锁完成后释放锁。pthread_mutex_lock(mutex);number;pthread_mutex_unlock(mutex);其中number是临界区执行它时不能允许其他线程同时进入同一受保护区域。5. 三种常见锁的定位录音和教材第 145 页先做了概念比较类型等待方式与并发规则入门阶段怎样理解互斥锁 Mutex同一时刻只有一个线程持有锁其他线程通常阻塞等待本节重点保护一般共享数据读写锁 Reader Writer Lock多个读者可同时进入写者必须独占读多写少时可能提高并发度自旋锁 Spinlock获取不到时循环忙等持续占用 CPU只适合持锁极短等特定场景常见于内核“读操作一定不需要锁”并不准确。多个线程只读且数据不会被任何线程修改时通常安全只要存在并发写入读者也必须参加同一套同步协议。6. 什么是互斥锁互斥锁mutex用于建立互斥访问任意时刻最多只有一个线程持有某一把锁。线程 B互斥锁线程 A线程 B互斥锁线程 A锁已被 A 持有B 阻塞pthread_mutex_lock获取成功修改共享数据pthread_mutex_lockpthread_mutex_unlockB 获得锁修改共享数据pthread_mutex_unlock互斥锁还建立了内存同步关系一个线程解锁前完成的共享数据修改在另一个线程随后成功获取同一把锁后可以被正确观察到。6.1 锁与共享资源的关系锁本身不会自动知道它保护哪个变量。这个对应关系由程序员建立并始终遵守/* 约定访问 number 时必须持有 number_mutex */staticintnumber;staticpthread_mutex_tnumber_mutexPTHREAD_MUTEX_INITIALIZER;如果某些代码访问number时加锁另一些代码绕过锁直接写程序仍然存在数据竞争。7.pthread_mutex_t应当怎样理解声明互斥锁时使用#includepthread.hpthread_mutex_tmutex;教材第 146 页展示了 glibc 中pthread_mutex_t的联合体实现这是帮助观察实现方式的材料。编写应用程序时应把它当作不透明类型只包含pthread.h不直接访问内部字段不假设它在所有系统上都是同一种结构通过pthread_mutex_*系列函数操作。8. 互斥锁的五个基础操作intpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);intpthread_mutex_lock(pthread_mutex_t*mutex);intpthread_mutex_trylock(pthread_mutex_t*mutex);intpthread_mutex_unlock(pthread_mutex_t*mutex);intpthread_mutex_destroy(pthread_mutex_t*mutex);操作作用初学阶段重点pthread_mutex_init运行时初始化互斥锁attr传NULL表示默认属性pthread_mutex_lock获取锁锁被其他线程持有时阻塞等待pthread_mutex_trylock尝试获取锁获取不到立即返回EBUSYpthread_mutex_unlock释放当前线程持有的锁临界区结束后尽快释放pthread_mutex_destroy销毁不再使用且已解锁的锁销毁后不能继续使用除非重新初始化这些函数成功返回0失败直接返回错误号。检查方式是interrorpthread_mutex_lock(mutex);if(error!0){fprintf(stderr,pthread_mutex_lock: %s\n,strerror(error));}不要仅用perror()读取errno来判断大多数 Pthreads API 的错误。9. 两种初始化方式9.1 静态初始化适合默认属性、生命周期清晰的互斥锁staticpthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;使用这种方式后不再对同一把锁调用pthread_mutex_init()。9.2 动态初始化适合运行时创建的对象或需要自定义互斥锁属性的情况pthread_mutex_tmutex;interrorpthread_mutex_init(mutex,NULL);if(error!0){fprintf(stderr,pthread_mutex_init: %s\n,strerror(error));}使用结束且确认没有线程继续访问时pthread_mutex_destroy(mutex);静态初始化表示“不需要调用pthread_mutex_init()”并不等于互斥锁永远不能或永远不必销毁。若锁与进程同寿命进程退出时由操作系统回收资源省略显式销毁通常没有实际影响如果对象会被重复创建和释放就应在安全时调用pthread_mutex_destroy()。10. 基础完整练习四个线程安全累加下面用 4 个线程每个线程累加 100000 次。它比一次创建 20000 个线程更适合反复练习。#includepthread.h#includestdio.h#includestdlib.h#includestring.h#defineTHREAD_COUNT4#defineADD_PER_THREAD100000staticlongcounter0;staticpthread_mutex_tcounter_mutexPTHREAD_MUTEX_INITIALIZER;staticvoidcheck_pthread(interror,constchar*operation){if(error!0){fprintf(stderr,%s: %s\n,operation,strerror(error));exit(EXIT_FAILURE);}}staticvoid*add_thread(void*arg){(void)arg;for(inti0;iADD_PER_THREAD;i){check_pthread(pthread_mutex_lock(counter_mutex),pthread_mutex_lock);counter;/* 临界区读、改、写共享变量 */check_pthread(pthread_mutex_unlock(counter_mutex),pthread_mutex_unlock);}returnNULL;}intmain(void){pthread_tthreads[THREAD_COUNT];for(inti0;iTHREAD_COUNT;i){check_pthread(pthread_create(threads[i],NULL,add_thread,NULL),pthread_create);}for(inti0;iTHREAD_COUNT;i){check_pthread(pthread_join(threads[i],NULL),pthread_join);}printf(counter %ld期望值 %d\n,counter,THREAD_COUNT*ADD_PER_THREAD);check_pthread(pthread_mutex_destroy(counter_mutex),pthread_mutex_destroy);returnEXIT_SUCCESS;}编译运行gcc-Wall-Wextra-pthreadmutex_counter.c-omutex_counter ./mutex_counter预期输出counter 400000期望值 40000010.1 逐段理解counter是所有线程共享的数据counter_mutex是所有线程共同使用的同一把锁每次修改前调用pthread_mutex_lock()同一时刻只有一个线程能执行counter修改完成马上调用pthread_mutex_unlock()主线程join全部工作线程后才读取最终结果所有线程都结束后互斥锁一定无人使用此时才销毁。10.2 反向实验暂时注释掉lock和unlock两行多运行几次并观察结果。这个实验只能用于观察错误现象不能据此认为某次结果正确就代表程序安全。11.pthread_mutex_trylock()的使用方式pthread_mutex_lock()获取不到锁时等待pthread_mutex_trylock()获取不到时立即返回。#includeerrno.hinterrorpthread_mutex_trylock(counter_mutex);if(error0){counter;pthread_mutex_unlock(counter_mutex);}elseif(errorEBUSY){puts(锁正在使用本次先处理其他任务);}else{fprintf(stderr,pthread_mutex_trylock: %s\n,strerror(error));}trylock适合“当前拿不到锁也可以做别的事情”的逻辑。不要用高频循环不停调用trylock否则会退化成浪费 CPU 的忙等。12. 临界区应当多大加锁范围必须覆盖一次完整的共享状态操作但应尽量短。/* 推荐只保护共享数据修改 */pthread_mutex_lock(mutex);shared_value;pthread_mutex_unlock(mutex);/* 与共享状态无关的耗时工作放在锁外 */do_slow_work();锁范围太小会漏掉需要保护的操作范围太大则让其他线程长时间等待降低并发效率。不要在持锁期间随意执行长时间阻塞操作例如等待用户输入、长时间网络访问或不受控的休眠。13. 加锁后为什么看起来变慢录音中加锁后多个线程对计数器的修改被串行化所以这部分不再并行。这个代价换来了正确、可预测的共享状态。优化顺序应是先保证同步正确再测量真正的性能瓶颈缩短临界区或减少共享写入确有必要时再考虑原子操作、读写锁或任务拆分。“线程越多越快”并不成立。只做一次加一却创建一个线程线程管理成本远大于计算本身。14. 常见错误与后果错误可能后果正确做法只在部分访问位置加锁仍然存在数据竞争所有访问者遵守同一锁约定加锁后提前return没有解锁其他线程永久等待每条退出路径都释放锁同一普通互斥锁重复加锁当前线程可能把自己锁死明确锁的持有状态由未持锁线程调用unlock行为未定义或返回错误谁成功加锁谁负责解锁销毁仍被持有或等待中的锁行为未定义所有线程结束且锁已释放后销毁复制pthread_mutex_t复制后的对象不可可靠使用初始化并传递原对象地址用sleep猜测访问顺序时序仍不确定使用锁、条件变量或join忽略 Pthreads 返回值资源失败时继续错误流程检查非零错误号并处理一把锁包住所有耗时操作并发度下降只保护必要的共享状态15. 死锁的最小认识互斥锁使用错误可能造成死锁线程永久等待无法再发生的解锁操作。最简单的情况是同一线程对普通互斥锁连续加两次pthread_mutex_lock(mutex);pthread_mutex_lock(mutex);/* 可能永远等待自己释放 */多个锁还可能形成循环等待线程 A 持有锁 1等待锁 2 线程 B 持有锁 2等待锁 1入门阶段先遵守两条规则固定多把锁的获取顺序临界区结束后立即解锁。16. 录音与教材表述校正转写中的“静态条件”应为“竞态条件race condition”。number在并发无同步时不是简单的“答案偶尔少一点”而是 C 语言层面的数据竞争和未定义行为。pthread_join()负责等待与回收目标线程不能让多个工作线程自动互斥。pthread_mutex_t的内部结构属于具体 C 库实现应用代码只应包含pthread.h并调用公开 API。Pthreads 函数失败通常返回非零错误号不应概括成“负数表示失败”。pthread_mutex_trylock()在普通的“锁已被占用”情况下返回EBUSY不会阻塞等待。普通互斥锁等待时通常阻塞不像自旋锁那样持续忙等具体调度由系统实现负责。静态初始化解决的是初始化方式。锁是否显式销毁取决于对象生命周期和程序结构。不能假定互斥锁释放后由哪一个等待线程先获得调度次序不是普通互斥锁提供的业务顺序保证。17. 本节最低掌握标准学完后应能回答什么是共享资源、竞态条件和临界区为什么一行number仍然可能被其他线程打断pthread_join()为什么不能解决工作线程之间的数据竞争pthread_mutex_lock()和pthread_mutex_trylock()有什么区别静态初始化与动态初始化分别怎样写为什么所有访问同一共享变量的代码都必须遵守同一把锁为什么不能销毁仍在使用的互斥锁为什么临界区既不能漏保护也不宜过大最低实践要求能独立写出“初始化 → 加锁 → 修改 → 解锁 → join → 销毁”的程序能把示例中的锁去掉并解释错误原因能用strerror(error)输出 Pthreads 错误能识别忘记解锁、重复加锁和错误销毁三类问题。18. PDF 页码与录音时间索引主题PDF 页码录音时间竞态条件定义14200:09—01:2120000 线程累加示例143—14401:22—04:37结果不稳定及原因14504:38—06:21避免共享写入与加锁14506:00—06:21互斥锁、读写锁、自旋锁概览14506:22—08:06pthread_mutex_t与互斥锁用途14608:07—09:20加锁、尝试加锁、解锁与销毁第 146 页主题的录音扩展09:21—12:22使用互斥锁修正累加程序录音演示12:23—15:37进一步核对资料POSIX Programmer’s Manualpthread_mutex_init / destroyPOSIX Programmer’s Manualpthread_mutex_lock / trylock / unlock19. 下一步学习掌握本节后下一步通常学习条件变量或其他线程同步方法。继续学习前先确保自己能独立完成第 10 节的安全计数器因为后续“等待某个条件成立”的写法通常仍要配合互斥锁使用。