临界区是什么?
在服务器后台开发中,多个线程同时操作共享资源是家常便饭。比如一个订单系统,两个线程同时修改同一笔库存数量,结果可能就是数据错乱——一个减了,另一个也减,但基于的是旧值,最终少减了一次。这种共享资源的访问区域,就叫“临界区”。
问题场景:抢票系统的尴尬
想象一个高并发的抢票服务。100个用户同时请求购买最后一张票,每个线程都先查库存是否大于0,再扣减。如果没有同步机制,所有线程可能在同一时刻读到“还有1张”,然后全都执行扣减,结果库存变成-99。这显然不行。这时候就得靠线程同步来保护临界区代码。
如何保护临界区?
最常见的方式是使用互斥锁(Mutex)。进入临界区前加锁,出来后解锁。同一时间只有一个线程能持有锁,也就保证了临界区的串行执行。
#include <pthread.h>
int ticket_count = 1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* buy_ticket(void* arg) {
pthread_mutex_lock(&lock); // 进入临界区,加锁
if (ticket_count > 0) {
usleep(1000); // 模拟处理延迟
ticket_count--; // 扣减库存
}
pthread_mutex_unlock(&lock); // 离开临界区,解锁
return NULL;
}
上面这段C代码就是一个典型的临界区保护示例。即使多个线程同时调用 buy_ticket,mutex确保了每次只有一个线程能执行判断和扣减操作,避免了超卖。
别让锁成了性能瓶颈
虽然锁能解决问题,但滥用也会拖慢系统。比如把整个函数都包进临界区,哪怕其中大部分代码根本不涉及共享数据,这就属于“过度保护”。应该只锁真正需要的部分,越短越好。
另外,死锁也是常见陷阱。两个线程各自拿着一把锁,又去等对方的锁,结果谁都动不了。写代码时要约定好锁的获取顺序,避免交叉等待。
其他同步机制简要对比
除了互斥锁,还有信号量、读写锁、自旋锁等。信号量适合控制资源数量,比如限制最多10个线程同时访问数据库连接池;读写锁在读多写少的场景更高效,允许多个读线程并发进入;自旋锁则适合极短的临界区,避免线程切换开销。
但在大多数情况下,互斥锁配合良好的临界区划分,已经能解决服务器程序中的绝大多数同步问题。