Linux C语言互斥锁实现线程同步的策略
1. 线程同步问题概述
在多线程编程中,线程同步是一个至关重要的问题。当多个线程同时访问和修改共享资源时,如果没有适当的同步机制,就可能导致数据竞争(Data Race)问题,进而引发程序出现不可预测的行为,比如数据损坏、程序崩溃等。
想象一下,多个线程同时对一个全局变量进行累加操作。如果没有同步机制,一个线程可能在读取该变量的值之后,还未来得及更新它,另一个线程又读取了相同的值,这样就会导致累加操作丢失,最终得到的结果并非预期值。
2. 互斥锁简介
互斥锁(Mutex,即 Mutual Exclusion 的缩写)是一种最基本的线程同步机制,用于保证在同一时刻只有一个线程能够访问共享资源,从而避免数据竞争。
从概念上讲,互斥锁就像是一把钥匙,当一个线程获取了这把钥匙(锁定互斥锁),它就可以进入临界区(访问共享资源的代码段)。在该线程释放钥匙(解锁互斥锁)之前,其他线程无法获取钥匙进入临界区。
在 Linux C 语言编程中,互斥锁相关的操作主要由 POSIX 线程库(pthread 库)提供支持。
3. 互斥锁的创建与初始化
在使用互斥锁之前,需要先创建并初始化它。在 POSIX 线程库中,可以使用 pthread_mutex_init
函数来完成这一操作。
#include <pthread.h>
// 定义一个互斥锁变量
pthread_mutex_t mutex;
int main() {
// 初始化互斥锁
int ret = pthread_mutex_init(&mutex, NULL);
if (ret != 0) {
perror("pthread_mutex_init");
return 1;
}
// 后续代码...
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
上述代码中,pthread_mutex_init
函数的第一个参数是指向互斥锁变量的指针,第二个参数是一个可选的属性指针,通常设置为 NULL
,表示使用默认属性。
如果初始化成功,函数返回 0
;否则返回一个非零错误码,通过 perror
函数可以打印出具体的错误信息。
4. 互斥锁的锁定与解锁
一旦互斥锁初始化完成,线程就可以通过 pthread_mutex_lock
函数来锁定互斥锁,进入临界区;通过 pthread_mutex_unlock
函数来解锁互斥锁,离开临界区。
#include <pthread.h>
#include <stdio.h>
// 定义共享资源
int shared_variable = 0;
// 定义一个互斥锁变量
pthread_mutex_t mutex;
void* increment(void* arg) {
// 锁定互斥锁
pthread_mutex_lock(&mutex);
for (int i = 0; i < 1000000; ++i) {
shared_variable++;
}
// 解锁互斥锁
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_t thread1, thread2;
// 创建线程1
pthread_create(&thread1, NULL, increment, NULL);
// 创建线程2
pthread_create(&thread2, NULL, increment, NULL);
// 等待线程1结束
pthread_join(thread1, NULL);
// 等待线程2结束
pthread_join(thread2, NULL);
printf("Final value of shared_variable: %d\n", shared_variable);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
在上述代码中,increment
函数是线程执行的函数体。在函数开始处,通过 pthread_mutex_lock
锁定互斥锁,这样其他线程就无法同时进入该临界区。在对共享变量 shared_variable
进行累加操作完成后,通过 pthread_mutex_unlock
解锁互斥锁,允许其他线程获取锁并访问共享资源。
在 main
函数中,创建了两个线程,它们都调用 increment
函数。如果没有互斥锁,两个线程同时对 shared_variable
进行累加操作,很可能会导致数据竞争,最终得到的结果是不确定的。而使用互斥锁后,就可以保证 shared_variable
的累加操作是正确的。
5. 互斥锁的错误处理
在使用互斥锁的过程中,错误处理是必不可少的。pthread_mutex_lock
和 pthread_mutex_unlock
函数都可能返回错误码。
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
// 定义共享资源
int shared_variable = 0;
// 定义一个互斥锁变量
pthread_mutex_t mutex;
void* increment(void* arg) {
int ret;
// 锁定互斥锁
ret = pthread_mutex_lock(&mutex);
if (ret != 0) {
errno = ret;
perror("pthread_mutex_lock");
return NULL;
}
for (int i = 0; i < 1000000; ++i) {
shared_variable++;
}
// 解锁互斥锁
ret = pthread_mutex_unlock(&mutex);
if (ret != 0) {
errno = ret;
perror("pthread_mutex_unlock");
return NULL;
}
return NULL;
}
int main() {
int ret;
// 初始化互斥锁
ret = pthread_mutex_init(&mutex, NULL);
if (ret != 0) {
errno = ret;
perror("pthread_mutex_init");
return 1;
}
pthread_t thread1, thread2;
// 创建线程1
ret = pthread_create(&thread1, NULL, increment, NULL);
if (ret != 0) {
errno = ret;
perror("pthread_create thread1");
return 1;
}
// 创建线程2
ret = pthread_create(&thread2, NULL, increment, NULL);
if (ret != 0) {
errno = ret;
perror("pthread_create thread2");
return 1;
}
// 等待线程1结束
ret = pthread_join(thread1, NULL);
if (ret != 0) {
errno = ret;
perror("pthread_join thread1");
return 1;
}
// 等待线程2结束
ret = pthread_join(thread2, NULL);
if (ret != 0) {
errno = ret;
perror("pthread_join thread2");
return 1;
}
printf("Final value of shared_variable: %d\n", shared_variable);
// 销毁互斥锁
ret = pthread_mutex_destroy(&mutex);
if (ret != 0) {
errno = ret;
perror("pthread_mutex_destroy");
return 1;
}
return 0;
}
在上述代码中,对 pthread_mutex_init
、pthread_mutex_lock
、pthread_mutex_unlock
、pthread_create
和 pthread_join
等函数的返回值都进行了检查。如果函数返回非零错误码,通过 errno
和 perror
函数可以打印出具体的错误信息,方便调试和定位问题。
6. 死锁问题与避免
死锁是多线程编程中一个棘手的问题,它发生在两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行的情况。
考虑以下场景:线程 A 持有互斥锁 mutex1
并试图获取互斥锁 mutex2
,而线程 B 持有互斥锁 mutex2
并试图获取互斥锁 mutex1
,此时就会发生死锁。
#include <pthread.h>
#include <stdio.h>
// 定义两个互斥锁变量
pthread_mutex_t mutex1, mutex2;
void* thread_function1(void* arg) {
// 锁定互斥锁1
pthread_mutex_lock(&mutex1);
printf("Thread 1 has locked mutex1\n");
// 尝试锁定互斥锁2
pthread_mutex_lock(&mutex2);
printf("Thread 1 has locked mutex2\n");
// 解锁互斥锁2
pthread_mutex_unlock(&mutex2);
// 解锁互斥锁1
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread_function2(void* arg) {
// 锁定互斥锁2
pthread_mutex_lock(&mutex2);
printf("Thread 2 has locked mutex2\n");
// 尝试锁定互斥锁1
pthread_mutex_lock(&mutex1);
printf("Thread 2 has locked mutex1\n");
// 解锁互斥锁1
pthread_mutex_unlock(&mutex1);
// 解锁互斥锁2
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main() {
// 初始化互斥锁1
pthread_mutex_init(&mutex1, NULL);
// 初始化互斥锁2
pthread_mutex_init(&mutex2, NULL);
pthread_t thread1, thread2;
// 创建线程1
pthread_create(&thread1, NULL, thread_function1, NULL);
// 创建线程2
pthread_create(&thread2, NULL, thread_function2, NULL);
// 等待线程1结束
pthread_join(thread1, NULL);
// 等待线程2结束
pthread_join(thread2, NULL);
// 销毁互斥锁1
pthread_mutex_destroy(&mutex1);
// 销毁互斥锁2
pthread_mutex_destroy(&mutex2);
return 0;
}
在上述代码中,如果线程 1 先获取了 mutex1
,线程 2 先获取了 mutex2
,然后它们各自尝试获取对方持有的锁,就会陷入死锁。
为了避免死锁,可以遵循以下原则:
- 按顺序加锁:所有线程都按照相同的顺序获取锁。例如,在上述例子中,如果所有线程都先获取
mutex1
,再获取mutex2
,就可以避免死锁。 - 使用超时机制:使用
pthread_mutex_timedlock
函数替代pthread_mutex_lock
函数,设置一个超时时间。如果在规定时间内无法获取锁,线程可以放弃获取并采取其他措施,而不是一直等待。
7. 递归互斥锁
普通互斥锁如果被同一个线程多次锁定,会导致死锁。因为第二次锁定时,该线程已经持有锁,再次获取锁会被阻塞,从而导致自身死锁。
递归互斥锁(Recursive Mutex)则允许同一个线程对其进行多次锁定,每锁定一次,内部的引用计数就加 1,解锁时引用计数减 1,只有当引用计数为 0 时,互斥锁才真正被释放。
在 POSIX 线程库中,可以通过设置互斥锁的属性来创建递归互斥锁。
#include <pthread.h>
#include <stdio.h>
// 定义共享资源
int shared_variable = 0;
// 定义一个互斥锁变量
pthread_mutex_t mutex;
// 定义互斥锁属性变量
pthread_mutexattr_t attr;
void recursive_function(int count) {
if (count <= 0) return;
// 锁定互斥锁
pthread_mutex_lock(&mutex);
printf("Entering recursive_function with count %d\n", count);
shared_variable++;
recursive_function(count - 1);
// 解锁互斥锁
pthread_mutex_unlock(&mutex);
}
int main() {
// 初始化互斥锁属性
pthread_mutexattr_init(&attr);
// 设置互斥锁属性为递归
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化互斥锁
pthread_mutex_init(&mutex, &attr);
recursive_function(5);
printf("Final value of shared_variable: %d\n", shared_variable);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
// 销毁互斥锁属性
pthread_mutexattr_destroy(&attr);
return 0;
}
在上述代码中,recursive_function
是一个递归函数,它在每次递归调用时都会锁定互斥锁。由于使用的是递归互斥锁,不会发生死锁。
8. 读写锁与互斥锁的对比
读写锁(Read - Write Lock)是一种特殊的同步机制,它区分了读操作和写操作。允许多个线程同时进行读操作,因为读操作不会修改共享资源,不会导致数据竞争。但是,当有一个线程进行写操作时,其他线程无论是读操作还是写操作都必须等待,直到写操作完成。
相比之下,互斥锁则更为简单粗暴,无论是读操作还是写操作,都只允许一个线程进入临界区。
在一些读操作频繁而写操作较少的场景下,使用读写锁可以提高程序的并发性能。
#include <pthread.h>
#include <stdio.h>
// 定义共享资源
int shared_variable = 0;
// 定义读写锁变量
pthread_rwlock_t rwlock;
void* read_function(void* arg) {
// 锁定读锁
pthread_rwlock_rdlock(&rwlock);
printf("Thread %ld is reading: %d\n", (long)pthread_self(), shared_variable);
// 解锁读锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void* write_function(void* arg) {
// 锁定写锁
pthread_rwlock_wrlock(&rwlock);
shared_variable++;
printf("Thread %ld is writing: %d\n", (long)pthread_self(), shared_variable);
// 解锁写锁
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
pthread_t read_thread1, read_thread2, write_thread;
// 创建读线程1
pthread_create(&read_thread1, NULL, read_function, NULL);
// 创建读线程2
pthread_create(&read_thread2, NULL, read_function, NULL);
// 创建写线程
pthread_create(&write_thread, NULL, write_function, NULL);
// 等待读线程1结束
pthread_join(read_thread1, NULL);
// 等待读线程2结束
pthread_join(read_thread2, NULL);
// 等待写线程结束
pthread_join(write_thread, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
在上述代码中,read_function
函数通过 pthread_rwlock_rdlock
锁定读锁,允许多个读线程同时执行。write_function
函数通过 pthread_rwlock_wrlock
锁定写锁,在写操作时会阻止其他读线程和写线程进入。
9. 条件变量与互斥锁的结合使用
条件变量(Condition Variable)通常与互斥锁一起使用,用于线程间的同步。它可以让线程在满足特定条件时才继续执行。
例如,假设有一个生产者 - 消费者模型,生产者线程生产数据并放入共享缓冲区,消费者线程从共享缓冲区取出数据。当缓冲区为空时,消费者线程需要等待生产者线程生产数据;当缓冲区满时,生产者线程需要等待消费者线程取出数据。
#include <pthread.h>
#include <stdio.h>
#define BUFFER_SIZE 5
// 定义共享缓冲区
int buffer[BUFFER_SIZE];
// 定义缓冲区索引
int in = 0;
int out = 0;
// 定义互斥锁变量
pthread_mutex_t mutex;
// 定义条件变量变量
pthread_cond_t cond_full;
pthread_cond_t cond_empty;
void* producer(void* arg) {
for (int i = 0; i < 10; ++i) {
// 锁定互斥锁
pthread_mutex_lock(&mutex);
while ((in + 1) % BUFFER_SIZE == out) {
// 缓冲区满,等待
pthread_cond_wait(&cond_empty, &mutex);
}
buffer[in] = i;
printf("Produced: %d\n", buffer[in]);
in = (in + 1) % BUFFER_SIZE;
// 通知缓冲区非空
pthread_cond_signal(&cond_full);
// 解锁互斥锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; ++i) {
// 锁定互斥锁
pthread_mutex_lock(&mutex);
while (in == out) {
// 缓冲区空,等待
pthread_cond_wait(&cond_full, &mutex);
}
int data = buffer[out];
printf("Consumed: %d\n", data);
out = (out + 1) % BUFFER_SIZE;
// 通知缓冲区非满
pthread_cond_signal(&cond_empty);
// 解锁互斥锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 初始化条件变量
pthread_cond_init(&cond_full, NULL);
pthread_cond_init(&cond_empty, NULL);
pthread_t producer_thread, consumer_thread;
// 创建生产者线程
pthread_create(&producer_thread, NULL, producer, NULL);
// 创建消费者线程
pthread_create(&consumer_thread, NULL, consumer, NULL);
// 等待生产者线程结束
pthread_join(producer_thread, NULL);
// 等待消费者线程结束
pthread_join(consumer_thread, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
// 销毁条件变量
pthread_cond_destroy(&cond_full);
pthread_cond_destroy(&cond_empty);
return 0;
}
在上述代码中,producer
函数和 consumer
函数通过 pthread_cond_wait
函数等待特定条件(缓冲区空或满)的改变。pthread_cond_wait
函数会自动解锁互斥锁,并将线程置于等待状态,当条件变量被 pthread_cond_signal
或 pthread_cond_broadcast
唤醒时,线程会重新获取互斥锁并继续执行。
10. 互斥锁在实际项目中的应用场景
- 数据库访问:在多线程应用程序中访问数据库时,多个线程可能同时请求对数据库进行读写操作。使用互斥锁可以保证在同一时刻只有一个线程能够执行数据库操作,避免数据不一致问题。
- 文件操作:当多个线程需要同时对同一个文件进行读写时,互斥锁可以防止文件内容被破坏。例如,在日志记录场景中,多个线程可能需要向同一个日志文件中写入日志,通过互斥锁可以确保日志的完整性。
- 共享内存访问:在使用共享内存进行进程间通信(IPC)时,多个进程(或线程)可能会访问共享内存区域。互斥锁可以用于同步对共享内存的访问,保证数据的一致性。
通过合理使用互斥锁以及其他同步机制,开发人员可以编写出高效、稳定的多线程程序,充分发挥多核处理器的性能优势,提升软件的整体性能和用户体验。同时,对于复杂的多线程场景,还需要综合考虑死锁避免、性能优化等多方面因素,确保程序的健壮性和可靠性。