在Linux多线程编程中,理解共享资源和临界区的概念至关重要。想象一下多个线程就像超市收银台的多个收银员,他们都需要访问同一个库存系统来更新商品数量。如果没有适当的保护机制,两个收银员可能同时看到"最后一件商品"并都将其卖出,导致库存出现负数——这就是我们需要互斥机制解决的问题。
共享资源是多线程环境下被多个执行流共同访问的资源,比如全局变量、堆内存、文件描述符等。这些资源就像办公室里的公用打印机,所有人都能使用它。
临界资源是共享资源中需要特殊保护的部分。继续用打印机的例子,打印队列就是临界资源——如果两个人同时发送打印命令而没有协调机制,可能会导致文档内容混杂在一起。
临界区是线程中访问临界资源的代码段。这就像使用打印机前的设置操作——选择纸张、调整格式等,这些操作需要独占访问,不能被打断。
互斥机制确保任何时候只有一个线程能进入临界区,就像洗手间门上的"有人/无人"标志:
这种机制解决了**数据竞争(Data Race)**问题——当多个线程并发修改共享数据时导致的不一致状态。数据竞争就像两个人在同一时间编辑同一份文档而没有版本控制,最终内容将无法预测。
原子性操作是不可分割的操作,就像瞬间完成的交易,要么完全成功,要么完全失败,没有中间状态。在计算机中,即使是简单的i++操作也包含三个步骤:
如果没有原子性保证,两个线程可能交错执行这些步骤,导致最终结果不符合预期。现代CPU提供特殊的原子指令(如x86的LOCK前缀指令)来保证这些操作的完整性。
让我们通过一个售票系统的例子来理解互斥的必要性。假设有100张票,4个售票线程同时工作:
c复制int ticket = 100;
void *sell_ticket(void *arg) {
while(1) {
if(ticket > 0) {
usleep(1000); // 模拟售票耗时
printf("%s sells ticket:%d\n", (char*)arg, ticket);
ticket--;
} else {
break;
}
}
return NULL;
}
运行这个程序,你可能会看到如下异常输出:
code复制thread 1 sells ticket:100
thread 2 sells ticket:100
thread 3 sells ticket:99
thread 4 sells ticket:98
...
thread 1 sells ticket:-1
非原子操作:ticket--对应三条汇编指令:
asm复制mov (ticket), %eax ; 读取
sub $1, %eax ; 减1
mov %eax, (ticket) ; 写回
这些指令可能被其他线程打断。
检查与执行分离:if(ticket>0)检查和ticket--操作不是原子的,可能导致:
互斥量(Mutex)是解决这类问题的标准工具,它像一把钥匙,一次只允许一个线程持有:
c复制pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *sell_ticket(void *arg) {
while(1) {
pthread_mutex_lock(&lock);
if(ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", (char*)arg, ticket);
ticket--;
pthread_mutex_unlock(&lock);
} else {
pthread_mutex_unlock(&lock);
break;
}
}
return NULL;
}
现代处理器通过特殊指令实现互斥量,最常见的是比较并交换(CAS)指令:
asm复制; 伪代码展示锁的实现
lock:
mov $1, %eax ; 准备值1
xchg %eax, [lock] ; 原子交换
test %eax, %eax ; 测试原值
jnz lock ; 如果原值不为0,继续尝试
ret ; 获取锁成功
unlock:
mov $0, [lock] ; 释放锁
ret
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
c复制int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
c复制pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
// 使用...
pthread_mutex_destroy(mutex);
free(mutex);
c复制int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用时必须检查返回值:
c复制if(pthread_mutex_lock(&mutex) != 0) {
perror("Failed to lock mutex");
// 错误处理
}
c复制int pthread_mutex_trylock(pthread_mutex_t *mutex);
c复制int pthread_mutex_timedlock(pthread_mutex_t *mutex,
const struct timespec *abs_timeout);
c复制struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 2; // 2秒超时
if(pthread_mutex_timedlock(&mutex, &ts) == ETIMEDOUT) {
printf("获取锁超时\n");
}
互斥量有多种类型,通过属性设置:
c复制pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 设置递归互斥量
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
// 使用后销毁属性
pthread_mutexattr_destroy(&attr);
C++中可以使用RAII(Resource Acquisition Is Initialization)技术自动管理锁:
cpp复制class Mutex {
public:
Mutex() { pthread_mutex_init(&mutex_, nullptr); }
~Mutex() { pthread_mutex_destroy(&mutex_); }
void Lock() { pthread_mutex_lock(&mutex_); }
void Unlock() { pthread_mutex_unlock(&mutex_); }
private:
pthread_mutex_t mutex_;
};
class LockGuard {
public:
explicit LockGuard(Mutex& mutex) : mutex_(mutex) { mutex_.Lock(); }
~LockGuard() { mutex_.Unlock(); }
private:
Mutex& mutex_;
};
使用方式:
cpp复制Mutex mutex;
void safe_increment(int& value) {
LockGuard lock(mutex);
value++;
}
线程1:
c复制lock(A);
lock(B);
线程2:
c复制lock(B);
lock(A);
解决方案:固定锁的获取顺序
递归锁允许同一线程多次加锁:
c复制pthread_mutexattr_t attr;
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);
void recursive_func() {
pthread_mutex_lock(&mutex);
// 可以再次加锁
pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex);
}
减小临界区范围:只保护必要部分
c复制// 不好
pthread_mutex_lock(&lock);
complex_operation1();
complex_operation2();
pthread_mutex_unlock(&lock);
// 更好
complex_operation1();
pthread_mutex_lock(&lock);
critical_section();
pthread_mutex_unlock(&lock);
complex_operation2();
使用读写锁:当读多写少时
c复制pthread_rwlock_t rwlock;
pthread_rwlock_rdlock(&rwlock); // 读锁
pthread_rwlock_wrlock(&rwlock); // 写锁
自旋锁:对于非常短的临界区
c复制pthread_spinlock_t spinlock;
pthread_spin_lock(&spinlock);
// 非常短的操作
pthread_spin_unlock(&spinlock);
让我们实现一个完整的线程安全队列,展示互斥量的实际应用:
cpp复制#include <pthread.h>
#include <queue>
#include <iostream>
template<typename T>
class ThreadSafeQueue {
public:
ThreadSafeQueue() {
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadSafeQueue() {
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
void Push(const T& value) {
pthread_mutex_lock(&mutex_);
queue_.push(value);
pthread_cond_signal(&cond_);
pthread_mutex_unlock(&mutex_);
}
bool TryPop(T& value) {
pthread_mutex_lock(&mutex_);
if(queue_.empty()) {
pthread_mutex_unlock(&mutex_);
return false;
}
value = queue_.front();
queue_.pop();
pthread_mutex_unlock(&mutex_);
return true;
}
void WaitAndPop(T& value) {
pthread_mutex_lock(&mutex_);
while(queue_.empty()) {
pthread_cond_wait(&cond_, &mutex_);
}
value = queue_.front();
queue_.pop();
pthread_mutex_unlock(&mutex_);
}
size_t Size() const {
pthread_mutex_lock(&mutex_);
size_t size = queue_.size();
pthread_mutex_unlock(&mutex_);
return size;
}
private:
std::queue<T> queue_;
mutable pthread_mutex_t mutex_;
pthread_cond_t cond_;
};
这个实现展示了:
当程序挂起时,使用gdb检查线程状态:
bash复制gdb -p <pid>
thread apply all bt
查找卡在pthread_mutex_lock的线程,分析其调用栈。
valgrind --tool=drd:检测锁争用
bash复制valgrind --tool=drd ./your_program
perf lock:分析锁争用情况
bash复制perf lock record ./your_program
perf lock report
忘记释放锁:
锁的粒度不当:
优先级反转:
c复制pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
虽然互斥量是最常用的同步机制,但在特定场景下其他方案可能更合适:
对于简单变量,C11/C++11原子操作更高效:
cpp复制#include <atomic>
std::atomic<int> counter(0);
counter.fetch_add(1, std::memory_order_relaxed);
使用CAS(Compare-And-Swap)实现无锁数据结构:
cpp复制template<typename T>
class LockFreeStack {
struct Node {
T value;
Node* next;
};
std::atomic<Node*> head;
public:
void Push(const T& value) {
Node* new_node = new Node{value, nullptr};
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node));
}
bool Pop(T& value) {
Node* old_head = head.load();
while(old_head &&
!head.compare_exchange_weak(old_head, old_head->next));
if(!old_head) return false;
value = old_head->value;
delete old_head;
return true;
}
};
GCC支持的事务内存(实验性):
cpp复制__transaction_atomic {
// 原子执行的代码块
balance1 -= amount;
balance2 += amount;
}
让我们分析一个实际案例:一个多线程日志系统初始实现:
c复制pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
void log_message(const char* msg) {
pthread_mutex_lock(&log_mutex);
FILE* f = fopen("app.log", "a");
if(f) {
fprintf(f, "%s\n", msg);
fclose(f);
}
pthread_mutex_unlock(&log_mutex);
}
问题:
优化方案1:批处理
c复制void log_message_batch(const char* msg) {
static __thread char buffer[1024];
static __thread size_t pos = 0;
size_t len = strlen(msg);
if(pos + len + 2 > sizeof(buffer)) {
pthread_mutex_lock(&log_mutex);
FILE* f = fopen("app.log", "a");
if(f) {
fwrite(buffer, 1, pos, f);
fclose(f);
}
pthread_mutex_unlock(&log_mutex);
pos = 0;
}
memcpy(buffer + pos, msg, len);
pos += len;
buffer[pos++] = '\n';
}
优化方案2:专用日志线程
c复制ThreadSafeQueue<const char*> log_queue;
void* logger_thread(void*) {
FILE* f = fopen("app.log", "a");
if(!f) return NULL;
while(true) {
const char* msg;
log_queue.WaitAndPop(msg);
if(strcmp(msg, "EXIT") == 0) break;
fprintf(f, "%s\n", msg);
free((void*)msg); // 假设消息是malloc分配的
}
fclose(f);
return NULL;
}
void log_message_async(const char* msg) {
char* copy = strdup(msg);
log_queue.Push(copy);
}
不同系统对pthread的实现有差异:
可移植性建议:
c复制// 条件变量超时等待的可移植写法
struct timespec ts;
#ifdef __linux__
clock_gettime(CLOCK_REALTIME, &ts);
#else
struct timeval tv;
gettimeofday(&tv, NULL);
ts.tv_sec = tv.tv_sec;
ts.tv_nsec = tv.tv_usec * 1000;
#endif
ts.tv_sec += timeout_seconds;
pthread_cond_timedwait(&cond, &mutex, &ts);