☰
第 5 天:锁住的到底是什么?
2026/10/2 2:28:45 网站建设 项目流程

昨天,两条线程各做了十万次“加一”,结果却可能不到二十万。问题出在这里:读取旧值、计算、写回必须作为一个整体完成,另一条线程不能插进来。

最直接的办法,是给这段操作加一把锁:

pthread_mutex_lock(&mutex);counter++;pthread_mutex_unlock(&mutex);

两条线程必须使用同一把锁。一条线程拿到锁后,另一条要等它释放,才能进入这段代码。这种同一时刻只允许一个线程进入的区域,叫临界区。

锁并不是贴在counter上的封条。它保护的是程序员约定好的一段操作:所有访问这份共享数据的线程,都得遵守同一约定。只要有一处绕开锁,保护就可能失效。

不过,线程有时需要的不只是“别同时动数据”。它还需要等一句话变成真:货架上有东西,才能取;货架空了,才能放。

一格货架,两个线程

我们做一个只有一格的缓冲区:

  • 主线程负责生产数字1到5,一次放一个。
  • 工作线程负责取走数字。
  • slot存数字,full表示这一格是否已被占用。

这里要保护的其实是一条规则:full == 1时,slot里必须有一个可取的数字。修改slot和full的过程不能被另一条线程看到一半。

把下面代码保存为queue.c,在 Linux 或 WSL 中运行:

#include<pthread.h>#include<stdio.h>#include<string.h>#include<unistd.h>enum{COUNT=5};staticintslot;staticintfull=0;staticpthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER;staticpthread_cond_tnot_empty=PTHREAD_COND_INITIALIZER;staticpthread_cond_tnot_full=PTHREAD_COND_INITIALIZER;staticvoid*consume(void*arg){(void)arg;for(inti=0;i<COUNT;i++){pthread_mutex_lock(&mutex);while(!full){pthread_cond_wait(&not_empty,&mutex);}intitem=slot;full=0;pthread_cond_signal(&not_full);pthread_mutex_unlock(&mutex);printf("取走 %d\n",item);sleep(1);}returnNULL;}intmain(void){pthread_tconsumer;interror=pthread_create(&consumer,NULL,consume,NULL);if(error!=0){fprintf(stderr,"创建线程失败:%s\n",strerror(error));return1;}for(inti=1;i<=COUNT;i++){pthread_mutex_lock(&mutex);while(full){pthread_cond_wait(&not_full,&mutex);}slot=i;full=1;printf("放入 %d\n",i);pthread_cond_signal(&not_empty);pthread_mutex_unlock(&mutex);}pthread_join(consumer,NULL);return0;}

编译运行:

gcc-std=c11-Wall-Wextra-pthread-oqueue queue.c ./queue

你会看到数字依次被放入、取走。消费者每取一个都会暂停一秒,因此生产者很容易遇到“货架已满”,必须等消费者腾出位置。程序并不要求两个线程按某个固定时间交替运行;不管谁先拿到 CPU,都不能覆盖未取走的数字,也不能从空货架取东西。

等待时为什么要把锁放开?

看生产者的这几行:

pthread_mutex_lock(&mutex);while(full){pthread_cond_wait(&not_full,&mutex);}

如果货架满了,生产者不能一直拿着锁等待。消费者也需要这把锁,才能取走数字、把full改成0。生产者不放锁,消费者进不来;消费者进不来,货架就永远不会空。

pthread_cond_wait帮我们完成关键动作:放开锁并进入等待;被唤醒后,重新拿到锁,再从函数返回。“放锁并等待”作为一个配套动作完成,避免线程刚放开锁、还没开始等待时,错过另一条线程发出的通知。

消费者取走数字后调用pthread_cond_signal(&not_full),告诉等待“货架有空位”的线程:你可以起来再检查了。生产者放入数字时,也用not_empty通知消费者。

注意,“通知了”不等于“条件现在一定成立”。线程被唤醒、重新拿到锁之前,其他线程可能已改变状态;等待也可能无缘无故结束。因此代码用的是while,不是if:

while(full){pthread_cond_wait(&not_full,&mutex);}

醒来后再检查full。真空了才继续放;仍然满,就接着等。

**条件变量负责通知,full才是判断能否继续的依据。**如果发通知时没人等待,条件变量不会替你“存下一次通知”;共享状态仍要由程序自己记录。

为什么不用一个循环反复检查?

可以写出这样的代码:

while(full){/* 一直检查 */}

它看似也在等待,实际上可能持续消耗 CPU。如果还拿着互斥锁循环,消费者根本无法把full改掉;即使不拿锁,也得正确处理共享数据的同步问题。

条件变量让线程在条件不满足时睡眠,等状态改变再来检查。这和第 3 天讲的调度接上了:等货架空位的线程,此刻不需要 CPU 来做计算。

信号量又解决什么问题?

生产者—消费者问题也常用信号量来写。信号量内部维护一个计数;wait尝试把计数减一,没额度就等待;post把计数加一,并可能唤醒等待者。

对一格货架,可以设:

空位数 empty = 1 物品数 items = 0 生产者:等待 empty → 放入物品 → 增加 items 消费者:等待 items → 取走物品 → 增加 empty

互斥锁、条件变量和信号量的侧重点不同:

工具在这里解决的问题
互斥锁修改货架状态时,别让另一条线程同时修改
条件变量货架满或空时,睡眠并等待状态变化
信号量记录可用空位或物品的数量

如果扩展到多个生产者、多个消费者,只靠表示数量的信号量,还得仔细保护实际读写货架的过程。

这篇代码里最值得记住的是两条线:**改共享状态时拿锁;条件不满足时放锁等待,醒来再检查。**下一篇要看,如果两个线程各拿着一把锁,偏偏又等对方放锁,会发生什么。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询