C++ / a working model

136 / 163   ·   C11   ·   约 8 分钟

常见并发问题

先记住这句话

本章分析并发软件中反复出现的缺陷模式,重点区分死锁与非死锁问题,后者主要包括原子性假设失败和执行顺序颠倒。通过同步原语可有效缓解这些风险,提升多线程代码的可靠性。

本篇内容
  1. 缺陷模式的实证研究
  2. 原子性假设的破裂
  3. 执行顺序的意外翻转
  4. 循环等待的形成与破解
  5. 运行示例
  6. 动手练习

官方章节 PDF

缺陷模式的实证研究

对数据库、服务器和办公套件等成熟项目的审查表明,绝大多数并发缺陷并非死锁,而是源于对代码片段不可分割性的错误预期,或线程间事件先后关系未被强制保证。识别这些高频模式有助于开发者在编写阶段就规避问题。

原子性假设的破裂

当程序员认为一组内存操作必须连续完成,但调度器却允许其他线程插入时,就会出现原子性破裂。典型场景是先测试指针有效再解引用,中间却被清零。用互斥锁把测试与使用包在同一临界区内即可恢复预期的不可分割性。

执行顺序的意外翻转

某些操作必须先于另一些操作发生,例如对象必须先被完整构造才能被读取。若创建线程尚未返回句柄,使用线程就已开始访问,便会触发空指针或未定义行为。引入条件变量配合状态标志,可让后者明确等待前者完成,从而锁定正确时序。

循环等待的形成与破解

当多个线程各自持有一把锁并请求对方已持有的锁时,便形成无法前进的循环。最直接的预防是全局约定锁的获取次序,使环路根本无法出现;也可辅以超时放弃或运行时检测来恢复系统。

常见误区

  • 误以为单条if语句本身已具备原子性而省略锁
  • 线程创建后立即访问其返回的对象,未等待初始化信号
  • 不同代码路径以相反次序获取多把锁,埋下死锁隐患

运行一个例子

最低标准 C11 · 完整程序 · 下载 .c

#include <stdio.h>
#include <pthread.h>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static int ready = 0;

static void *producer(void *arg)
{ (void)arg;
    pthread_mutex_lock(&mtx);
    ready = 1;
    pthread_mutex_unlock(&mtx);
    return NULL;
}

static void *consumer(void *arg)
{ (void)arg;
    pthread_mutex_lock(&mtx);
    if (ready) {
        /* safe access under lock */
    }
    pthread_mutex_unlock(&mtx);
    return NULL;
}

int main(void)
{
    pthread_t tprod, tcons;
    pthread_create(&tprod, NULL, producer, NULL);
    pthread_create(&tcons, NULL, consumer, NULL);
    pthread_join(tprod, NULL);
    pthread_join(tcons, NULL);
    printf("Concurrency demo finished successfully.\n");
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-32-concurrency-bugs.c -o example && ./example

预期结果

Concurrency demo finished successfully.

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

一个线程在检查共享指针非空后立刻打印其内容,另一线程可能在同一时刻将其置空。这属于哪类缺陷?应如何用锁修复?

查看参考答案

这是原子性违反。修复方法是用同一把互斥锁把“检查”和“打印”整段包起来,确保中间不可能被置空。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录