
1. 项目概述从“锁”的比喻到并发编程的基石在Linux系统编程的世界里尤其是当你开始涉足多线程、多进程开发时有一个概念会像幽灵一样反复出现它就是“互斥锁”。很多新手朋友第一次听到这个词可能会觉得它既神秘又复杂仿佛是什么高深的魔法。其实它的核心思想非常朴素就像我们日常生活中随处可见的“锁”。想象一下公共厕所的那个小隔间门上有个可以拨动的“有人/无人”的锁扣。当一个人进去后他会把锁扣拨到“有人”状态这就在告诉后来者“我正在使用请等待”。这个简单的动作防止了两个人同时进入一个隔间的尴尬和混乱。Linux中的互斥锁干的就是这个事儿只不过它守护的不是厕所隔间而是一段被称为“临界区”的代码或一块共享数据。那么互斥锁到底是什么用一句最直白的话说互斥锁是一种用于实现线程或进程间同步的机制其核心作用是确保在任意时刻只有一个执行流线程或进程能够进入被保护的临界区访问共享资源。这里的“互斥”指的就是“互相排斥”你进我就不能进。为什么需要这种机制因为现代计算机为了提升效率普遍采用了并发执行模型。你的程序可能创建多个线程同时运行或者操作系统本身就在调度多个进程。当这些并发的执行流去操作同一块内存、同一个文件、同一个硬件设备时如果没有协调机制就会引发“竞态条件”。竞态条件就像一场没有裁判的百米赛跑多个选手线程冲向终点共享资源最终谁先撞线、结果如何完全取决于微妙的时机每次运行都可能不同导致程序行为不可预测数据被破坏。互斥锁就是那位公正的裁判它让选手们排队一次只允许一个人冲刺。理解并熟练运用互斥锁是写出正确、健壮、高效的并发程序的必修课。无论你是做服务器后端开发、高性能计算还是嵌入式系统都绕不开它。接下来我们就深入拆解这个看似简单实则内涵丰富的同步原语。2. 互斥锁的核心原理与工作机制要真正用好互斥锁不能只停留在“知道它能锁东西”的层面必须理解它的内部状态机和工作原理。这就像开车知道踩油门能走是基础但了解发动机和变速箱如何协同工作才能开得更好、更安全。2.1 互斥锁的两种基本状态一个互斥锁对象在任意时刻通常只处于以下两种状态之一锁定Locked也称为“占用”状态。表示已经有某个线程成功获取加锁了这个锁正在独占性地访问受保护的临界区资源。此时锁就像那扇被关上的厕所门。未锁定Unlocked也称为“空闲”或“释放”状态。表示当前没有线程持有该锁临界区处于可进入状态。后来的线程可以尝试去获取它。这个状态转换是互斥锁所有行为的基础。线程通过调用特定的API如pthread_mutex_lock尝试从“未锁定”状态切换到“锁定”状态。如果切换成功线程就持有了锁如果失败因为锁已被其他线程持有线程通常会进入等待。2.2 关键操作加锁与解锁的底层逻辑让我们深入到一次典型的加锁-解锁流程中看看会发生什么。假设我们使用POSIX线程库pthread中最常见的互斥锁。加锁pthread_mutex_lock 当一个线程调用pthread_mutex_lock(mutex)时底层会发生一系列原子操作原子操作意味着这个操作要么完全执行要么完全不执行不会被其他线程打断检查锁状态CPU会检查mutex变量当前的值。在简单的实现中这可能就是一个整数0表示空闲1表示占用或者是一个指向持有者线程的指针。空闲则获取如果检查发现锁是空闲的例如值为0CPU会原子性地将其设置为占用状态例如设置为1或记录当前线程ID。这个“检查并设置”的操作必须是原子的这是实现互斥的基石。如果这一步成功函数立即返回线程成功获取锁可以安全进入临界区。占用则等待如果检查发现锁已被占用那么调用线程无法立即获得锁。此时根据互斥锁的属性我们后面会详谈线程的行为会不同默认阻塞线程会将自己挂起进入睡眠状态让出CPU。操作系统会将其放入一个与该锁关联的等待队列中。这避免了忙等待即循环空转检查锁状态节省了宝贵的CPU资源。非阻塞如果锁被设置为PTHREAD_MUTEX_NONBLOCKING属性函数会立即返回一个错误码如EBUSY而不是等待。这给了调用者“尝试获取失败则做别的事”的选择。解锁pthread_mutex_unlock 当持有锁的线程完成临界区操作后调用pthread_mutex_unlock(mutex)释放锁原子性地将锁状态恢复为“空闲”例如将值从1改回0或清空持有者信息。唤醒等待者操作系统会检查该锁的等待队列。如果队列中有其他线程在等待操作系统会从中选择一个选择策略可能是先入先出FIFO也可能是基于优先级将其状态设置为可运行并可能将其放入就绪队列。被唤醒的线程在下一次被调度时会再次尝试上述的加锁流程。注意这里描述的“检查并设置”是概念模型。在现代多核CPU体系结构下实际的实现要复杂得多会用到内存屏障、原子比较交换CAS等指令并考虑缓存一致性协议如MESI来确保在所有CPU核心上看到的锁状态是一致的。但对于理解互斥锁的行为这个简化模型已经足够。2.3 互斥锁与信号量、自旋锁的辨析初学者常常混淆互斥锁和其他同步机制尤其是信号量。理解它们的区别至关重要。互斥锁 vs 信号量所有权这是最根本的区别。互斥锁具有所有权概念。即哪个线程加的锁必须由哪个线程来解锁。这就像你用自己的钥匙锁了门必须用同一把钥匙才能开。信号量则没有这个限制一个线程postV操作了信号量可以由另一个线程waitP操作。计数互斥锁的状态只有0空闲和1占用。而信号量是一个计数器其值可以大于1用于控制同时访问某类资源的线程数量例如连接池中有10个连接可以用一个初始值为10的信号量来管理。用途互斥锁用于互斥保护临界区一次只进一个。信号量更常用于同步协调线程间的执行顺序或者管理一组数量有限的资源。简单类比互斥锁是“单间厕所的钥匙”只有一把。信号量是“公共泳池的入场券”发行了100张同时最多允许100人进入。互斥锁 vs 自旋锁等待策略当获取锁失败时互斥锁会让线程睡眠发生上下文切换。而自旋锁会让线程在一个循环里忙等待不断检查锁是否被释放“自旋”这个名字很形象。开销与场景线程睡眠和唤醒上下文切换是有开销的。因此如果预计等待锁的时间非常短比如在微秒级那么上下文切换的开销可能比忙等待更大。此时使用自旋锁更高效。反之如果等待时间可能较长使用互斥锁能避免白白消耗CPU周期。在用户态我们通常使用互斥锁自旋锁更多见于内核开发或一些极低延迟的用户态库中。Linux的pthread库也提供了自旋锁接口pthread_spinlock_t。特性互斥锁 (Mutex)信号量 (Semaphore)自旋锁 (Spinlock)核心目的互斥访问同步/资源计数互斥访问短时等待所有权有谁锁谁解无有通常计数器二进制 (0/1)非负整数二进制 (0/1)等待策略阻塞睡眠阻塞睡眠忙等待自旋适用场景保护临界区等待时间可能较长线程同步控制资源池大小保护临界区等待时间极短3. Linux中互斥锁的实战从创建到销毁理论讲得再多不如动手写一行代码。我们以POSIX线程pthread库为例看看互斥锁在C语言中如何被使用。这是最经典、最跨平台在Unix-like系统上的用法。3.1 互斥锁的创建与初始化在pthread中互斥锁的类型是pthread_mutex_t。它是一个不透明的数据类型我们通常不需要关心其内部结构只需知道如何声明和初始化它。有两种初始化方式静态初始化编译时 对于全局或静态的互斥锁可以使用宏PTHREAD_MUTEX_INITIALIZER进行初始化。这种方式最简单。#include pthread.h pthread_mutex_t global_mutex PTHREAD_MUTEX_INITIALIZER;这行代码在程序加载时就将global_mutex初始化为一个具有默认属性的、未锁定的互斥锁。注意静态初始化的互斥锁不需要也不能调用pthread_mutex_destroy来销毁。动态初始化运行时 更灵活的方式是使用pthread_mutex_init函数。这允许你指定互斥锁的属性。#include pthread.h #include stdio.h #include stdlib.h int main() { pthread_mutex_t mutex; // 使用NULL表示默认属性 int ret pthread_mutex_init(mutex, NULL); if (ret ! 0) { // 初始化失败ret是错误码 perror(pthread_mutex_init failed); exit(EXIT_FAILURE); } // ... 使用互斥锁 ... // 使用完毕后必须销毁 pthread_mutex_destroy(mutex); return 0; }pthread_mutex_init的第二个参数是一个指向pthread_mutexattr_t的指针可以用来设置锁的类型普通、检错、递归等、进程共享属性等。传入NULL则使用所有属性的默认值。实操心得初始化选择我个人的习惯是如果互斥锁的作用域局限于单个函数或某个模块内部且生命周期明确我会使用动态初始化并在作用域结束时销毁。如果是一个全局性的、在程序整个生命周期都需要的锁比如保护一个全局配置结构体我会使用静态初始化省去初始化和销毁的麻烦。但切记静态初始化的锁不能销毁动态初始化的锁必须销毁否则可能导致资源泄漏。3.2 加锁与解锁的代码范式加锁和解锁必须成对出现并且要确保在所有可能的执行路径上包括发生错误、提前返回时都能解锁否则会导致死锁。这是使用互斥锁时最容易出错的地方。基本范式pthread_mutex_lock(mutex); // 临界区开始 // ... 操作共享资源 ... // 临界区结束 pthread_mutex_unlock(mutex);更安全的范式使用pthread_cleanup_push/pop或lock_guard思想在C语言中没有RAII资源获取即初始化机制我们需要手动确保解锁。一个常见技巧是在可能提前返回如错误处理的地方都写上解锁语句。void some_function() { pthread_mutex_lock(mutex); if (some_error_condition) { pthread_mutex_unlock(mutex); // 错误分支也要解锁 return; } // 正常操作... pthread_mutex_unlock(mutex); }在C中我们可以利用RAII创建std::lock_guard或std::unique_lock对象在构造函数中加锁在析构函数中自动解锁即使发生异常也能保证锁被释放安全又省心。3.3 互斥锁的属性设置前面提到pthread_mutex_init可以设置属性。让我们看看几个关键属性类型TypePTHREAD_MUTEX_NORMAL默认普通锁。不提供死锁检测。如果一个线程对已锁定的普通锁再次加锁会导致死锁。同一个线程解锁一个未被自己锁定的锁或者解锁一个未锁定的锁行为是未定义的通常会导致程序崩溃。PTHREAD_MUTEX_ERRORCHECK检错锁。这种锁会进行错误检查。如果同一个线程试图对它已经持有的锁再次加锁函数会返回错误EDEADLK。这有助于调试死锁。解锁非持有锁等操作也会返回错误。PTHREAD_MUTEX_RECURSIVE递归锁。允许同一个线程对同一把锁多次加锁只要解锁次数与加锁次数匹配即可。这对于在递归函数中保护共享数据非常有用。注意递归锁的开销通常比普通锁稍大。PTHREAD_MUTEX_DEFAULT通常映射为PTHREAD_MUTEX_NORMAL。进程共享Process-sharedPTHREAD_PROCESS_PRIVATE默认锁只能被同一个进程内的线程共享。PTHREAD_PROCESS_SHARED锁可以被放置在不同进程的线程之间共享的内存中如共享内存shm。这用于进程间同步但设置和使用起来更复杂。设置属性的示例pthread_mutexattr_t attr; pthread_mutex_t mutex; pthread_mutexattr_init(attr); // 设置为递归锁 pthread_mutexattr_settype(attr, PTHREAD_MUTEX_RECURSIVE); // 设置为进程间共享需要锁在共享内存中 // pthread_mutexattr_setpshared(attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mutex, attr); // 使用完毕后销毁属性对象 pthread_mutexattr_destroy(attr);3.4 互斥锁的销毁对于动态初始化的互斥锁在使用完毕后必须调用pthread_mutex_destroy来释放其占用的任何资源。pthread_mutex_destroy(mutex);重要规则销毁一个已锁定或正在被线程等待的互斥锁其行为是未定义的。务必确保在没有任何线程持有或等待该锁时再进行销毁。销毁一个已经销毁的互斥锁行为也是未定义的。静态初始化的互斥锁PTHREAD_MUTEX_INITIALIZER不需要销毁。4. 互斥锁的高级话题与性能考量当你掌握了互斥锁的基本用法后就会遇到更复杂的情况和性能瓶颈。这部分内容决定了你写出的并发程序是“能用”还是“高效、健壮”。4.1 死锁成因与经典解决方案死锁是并发编程中最令人头疼的问题之一而互斥锁使用不当是导致死锁的主要原因。死锁通常发生在两个或多个线程互相等待对方持有的锁导致所有线程都无法继续执行。经典死锁场景ABBA死锁线程1锁定锁A - 尝试锁定锁B 线程2锁定锁B - 尝试锁定锁A 此时线程1在等B线程2在等A两者永远等下去。解决死锁的策略固定锁顺序Lock Ordering 这是最有效、最常用的预防策略。为系统中所有的锁定义一个全局的、严格的获取顺序。任何线程在需要获取多把锁时都必须按照这个顺序来申请。例如定义锁A的序号为1锁B的序号为2。那么所有线程都必须先申请锁A再申请锁B。这样上面ABBA的场景就不会发生因为线程2会先尝试锁A但已被线程1持有从而在锁A上等待不会先去锁B。实操难点在大型、复杂的系统中维护一个清晰的全局锁顺序非常困难尤其是当锁是动态创建的时候。尝试锁与超时Try-Lock and Timeout 使用非阻塞的加锁函数如pthread_mutex_trylock或带超时的加锁函数如pthread_mutex_timedlock。当无法立即获取锁时线程不是傻等而是释放已经持有的锁回退backoff一段时间后再重试或者去做其他事情。// 尝试锁示例 if (pthread_mutex_trylock(mutex_B) ! 0) { // 获取锁B失败 pthread_mutex_unlock(mutex_A); // 释放已持有的锁A // 可以选择休眠片刻或处理其他任务 usleep(1000); continue; // 重试整个加锁序列 }这种方式破坏了“持有并等待”的死锁条件但实现逻辑较复杂且可能引起活锁两个线程不断重复“获取-释放-重试”的过程。锁层次Lock Hierarchies 这是锁顺序策略的一种具体实现。为每个锁分配一个层级号。规则是线程只能获取层级号比当前持有的所有锁的层级号更高的锁。这实际上强制了一个自底向上的加锁顺序。如果违反可以在调试版本中触发断言。死锁检测 对于一些支持检错属性PTHREAD_MUTEX_ERRORCHECK的锁同一个线程重复加锁会立即返回错误这可以帮助发现一部分死锁自身造成的死锁。更复杂的死锁检测通常需要借助外部工具如helgrindValgrind工具套件的一部分、tsanThreadSanitizer等它们能在运行时检测出潜在的锁顺序问题。避坑技巧锁的粒度锁的粒度指的是锁保护的数据范围大小。粗粒度锁保护一大片数据简单但并发度低容易成为性能瓶颈。细粒度锁如为哈希表的每个桶配一把锁保护的数据少并发度高但管理复杂容易引发死锁。一个实用的建议是从较粗的粒度开始在性能测试中确认锁竞争成为瓶颈后再有计划地细化和拆分锁。不要过早优化。4.2 性能瓶颈锁竞争与优化手段当大量线程频繁争用同一把锁时锁本身就会成为系统的性能瓶颈这种现象称为锁竞争或锁拥堵。线程大部分时间都在等待锁而不是执行有效工作。如何识别锁竞争可以使用性能剖析工具如perf、SystemTap或者一些语言特有的分析器。观察指标包括线程在锁函数如pthread_mutex_lock上花费的CPU时间比例。锁的等待队列长度。上下文切换频率是否异常高。优化锁竞争的策略减少锁的持有时间 这是最根本的优化。仔细审查临界区代码把能不放在锁里的操作坚决移出去。例如内存分配、格式化字符串、复杂的计算等尽量在加锁前或解锁后完成。// 优化前整个函数都在锁内 void process_data_bad(data_t *data) { pthread_mutex_lock(data-mutex); // 耗时操作1数据准备可以移出去 // 耗时操作2核心计算必须保护 // 耗时操作3结果整理可以移出去 pthread_mutex_unlock(data-mutex); } // 优化后只保护核心部分 void process_data_good(data_t *data) { // 耗时操作1在锁外准备数据 intermediate_result_t prep prepare_data(data); pthread_mutex_lock(data-mutex); // 耗时操作2核心计算访问共享状态 do_core_calculation(data, prep); pthread_mutex_unlock(data-mutex); // 耗时操作3在锁外整理结果 finalize_result(prep); }降低锁的争用频率数据分片Sharding将共享数据拆分成多个独立的部分每个部分用单独的锁保护。例如一个全局的计数器可以拆分成每个CPU核心一个的局部计数器最后再汇总这能极大减少竞争。这就是per-CPU变量的思想。读写锁Read-Write Lock如果对共享数据的操作大部分是“读”少部分是“写”那么使用读写锁pthread_rwlock_t可以大幅提升并发度。它允许多个读者同时进入但写者是独占的。在读多写少的场景下性能提升显著。使用无锁数据结构 这是终极解决方案但实现难度极高。无锁编程利用CPU提供的原子操作如CAS, Compare-And-Swap来直接操作共享数据完全避免使用锁。它提供了更好的扩展性和抗干扰性一个线程挂掉不会阻塞其他线程但代码极其复杂且正确性难以证明。除非你对性能有极致的追求并且是并发编程专家否则建议使用成熟的第三方无锁库如liblfds而不是自己实现。选择更快的锁实现 在Linux中pthread_mutex_t默认可能使用futex快速用户态互斥锁实现它在无竞争时开销很小。在特定场景下可以考虑使用自旋锁pthread_spinlock_t适用于极短临界区、或者Linux特有的futex系统调用进行更底层的控制。4.3 条件变量互斥锁的最佳搭档互斥锁解决了互斥访问的问题但很多时候线程间还需要等待某个条件成立。例如一个生产者线程向缓冲区放数据一个消费者线程从缓冲区取数据。当缓冲区空时消费者需要等待当缓冲区满时生产者需要等待。单纯用互斥锁消费者线程可能会在“加锁-检查-发现空-解锁-循环”中空转忙等待浪费CPU。条件变量Condition Variable就是用来解决这个问题的。它允许线程在某个条件不满足时原子性地释放互斥锁并进入等待状态直到被其他线程唤醒。条件变量总是与一个互斥锁结合使用。典型的生产者-消费者模式pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; Queue buffer; // 共享缓冲区 // 生产者线程 void* producer(void* arg) { while (1) { Item item produce_item(); pthread_mutex_lock(mutex); while (buffer_is_full(buffer)) { // 必须用while循环检查条件 pthread_cond_wait(cond, mutex); // 等待“缓冲区非满”信号 } enqueue(buffer, item); pthread_mutex_unlock(mutex); pthread_cond_signal(cond); // 通知消费者可能有数据了 } return NULL; } // 消费者线程 void* consumer(void* arg) { while (1) { pthread_mutex_lock(mutex); while (buffer_is_empty(buffer)) { // 必须用while循环检查条件 pthread_cond_wait(cond, mutex); // 等待“缓冲区非空”信号 } Item item dequeue(buffer); pthread_mutex_unlock(mutex); pthread_cond_signal(cond); // 通知生产者可能有空间了 consume_item(item); } return NULL; }关键点解析pthread_cond_wait(cond, mutex)这个函数会原子地执行三个操作1) 释放互斥锁mutex2) 将当前线程挂起等待在条件变量cond上3) 在被唤醒后重新获取互斥锁mutex。原子性保证了在释放锁和进入等待状态之间不会有其他线程改变条件并发送信号导致信号丢失。为什么用while而不是if检查条件这是使用条件变量时最容易犯的错误。当线程被唤醒时条件可能已经不再成立虚假唤醒或者虽然条件成立但又被其他抢先的线程改变了。因此必须在一个循环中重新检查条件。pthread_cond_wait的官方手册也明确要求必须在循环中调用。pthread_cond_signal(cond)唤醒至少一个等待在该条件变量上的线程。pthread_cond_broadcast(cond)唤醒所有等待的线程。通常使用signal即可除非你知道需要唤醒所有等待者。条件变量和互斥锁的组合构成了线程间同步的强大工具是实现各种复杂并发模式如线程池、工作队列的基础。5. 常见问题、调试技巧与最佳实践实录即使理解了所有原理在实际编码和调试中你依然会遇到各种稀奇古怪的问题。这一部分是我多年踩坑经验的总结希望能帮你少走弯路。5.1 典型问题排查清单当你遇到多线程程序行为异常崩溃、卡死、数据错误时可以按照以下清单进行排查现象可能原因排查思路与工具程序卡死无响应1.死锁线程互相等待锁。2.活锁线程不断重试但无法进展。3.条件变量使用错误等待的条件永远不成立且没有线程发信号。1. 使用gdbattach到进程thread apply all bt查看所有线程的堆栈。死锁线程通常会卡在pthread_mutex_lock或pthread_cond_wait。2. 使用helgrind或tsan进行死锁检测。3. 检查条件变量的等待逻辑是否用了while循环信号是否在正确的时机发出。数据偶尔错误或不一致1.竞态条件对共享数据的访问没有正确同步。2.锁粒度不当保护了不该保护的数据或漏掉了该保护的数据。3.内存序问题在没有正确内存屏障的情况下跨线程访问非原子变量。1. 仔细审查所有访问共享数据的地方是否都用了正确的锁。2. 使用tsanThreadSanitizer进行数据竞争检测。这是最强大的工具之一能直接定位到发生竞争的具体代码行。3. 对于简单的计数器考虑使用原子操作__atomic_*内置函数或C11stdatomic.h。性能随线程数增加而下降锁竞争太多线程争用少数几把锁。1. 使用perf记录分析查看在锁函数上的耗时占比。2. 考虑使用读写锁、数据分片、无锁数据结构来降低竞争。3. 使用valgrind --tooldrd或lockstat工具分析锁的争用情况。程序随机崩溃Segmentation Fault1.访问已销毁的锁或条件变量。2.在未初始化的锁上操作。3.解锁非持有锁对于普通锁是未定义行为。1. 确保锁的生命周期管理正确特别是动态创建和销毁的锁。2. 使用PTHREAD_MUTEX_ERRORCHECK属性初始化锁它能在调试时捕获一些错误用法。3. 在代码中严格配对加锁解锁操作可以使用封装类C或代码审查来保证。5.2 调试工具与实战技巧GDBGNU Debuggerinfo threads查看所有线程。thread n切换到第n号线程。thread apply all bt打印所有线程的调用堆栈。这是分析死锁的首选命令你能看到每个线程卡在哪个函数、哪把锁上。set scheduler-locking on在单步调试时锁定调度器使只有当前调试的线程运行其他线程挂起。这对于观察竞态条件很有帮助但会改变程序并发行为。Valgrind 套件helgrind专门用于检测多线程程序中的同步错误包括死锁、数据竞争、误用POSIX线程API等。命令valgrind --toolhelgrind ./your_program。drdDRD另一个线程错误检测器与helgrind类似有时能检测出helgrind漏掉的问题且通常运行更快。注意Valgrind会显著降低程序运行速度慢10-50倍且对程序的内存布局有影响适合在测试环境使用。ThreadSanitizer (TSan)这是一个编译时插桩工具相比Valgrind它的运行时开销小得多通常2-5倍更适合做持续的集成测试。使用GCC或Clang编译时添加-fsanitizethread标志即可启用。它能非常精确地报告数据竞争的位置。strace和ltracestrace跟踪系统调用。你可以看到线程在哪些futex互斥锁和条件变量的底层实现系统调用上阻塞这有助于理解线程的等待状态。ltrace跟踪库函数调用可以看到对pthread_*系列函数的调用序列。5.3 从实践中总结的最佳实践锁的封装与RAII在C中永远不要直接调用lock()和unlock()。使用std::lock_guard或std::unique_lock。在C语言中可以尝试用宏或包装函数来模拟确保异常安全。避免在持有锁时调用外部代码你永远不知道你调用的那个函数内部会不会再去获取另一把锁可能导致死锁或者进行一个耗时的I/O操作极大延长锁的持有时间。如果必须调用请确保你了解它的行为。优先使用粗粒度锁证明需要后再细化细粒度锁是性能优化的结果而不是起点。过早优化是万恶之源。为锁和受保护的数据建立明确关系在代码注释或设计文档中清晰地说明每一把锁保护的是哪些数据。最好能将锁和它保护的数据封装在同一个结构体中。编写线程安全的函数而非依赖调用者一个函数如果会操作共享数据那么它内部就应该处理好同步而不是要求调用者在外面加锁。这符合“单一职责原则”减少出错可能。测试测试再测试多线程bug具有极强的随机性和不可重现性。需要设计压力测试让线程以不同的顺序和速度交错执行。使用TSan等工具进行常态化检测。互斥锁是并发编程中一个强大而基础的工具。它像一把双刃剑用好了可以构建出高效、稳定的并发程序用不好则会引入难以调试的bug和性能陷阱。理解其原理遵循最佳实践善用调试工具是每一位系统程序员成长的必经之路。希望这篇长文能成为你手边一份有价值的参考。在实际编码中如果遇到诡异的多线程问题不妨再回来看看这些原理和清单或许就能找到线索。