【Linux探索学习】第三十一弹——线程互斥与同步(下):深入理解确保线程安全的机制

1. 线程同步的基本概念
1.1 什么是线程同步?
线程同步是指多个线程在访问共享资源时,通过某种机制来协调它们的执行顺序,以避免数据竞争和不一致性问题。常见的线程同步机制包括互斥锁、条件变量、读写锁、信号量等。

1.2 为什么需要线程同步?
在多线程环境中,多个线程可能会同时访问共享资源。如果没有适当的同步机制,可能会导致以下问题:

数据竞争:多个线程同时修改同一数据,导致数据不一致。

死锁:多个线程相互等待对方释放资源,导致程序无法继续执行。

活锁:线程不断尝试获取资源但总是失败,导致程序无法继续执行。(活锁的具体讲解想了解的可以再搜一下)

2. 条件变量
2.1 条件变量的基本概念
条件变量是一种线程同步机制,用于在多个线程之间传递信号。条件变量通常与互斥锁一起使用,用于在某个条件成立时唤醒等待的线程。

2.2 条件变量的相关函数详解
2.2.1 pthread_cond_init
函数原型:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
功能:
初始化条件变量。

参数:

cond:指向条件变量的指针。

attr:条件变量的属性,通常设置为NULL,表示使用默认属性。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
2.2.2 pthread_cond_destroy
函数原型:

int pthread_cond_destroy(pthread_cond_t *cond);
功能:
销毁条件变量。

参数:

cond:指向条件变量的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_cond_destroy(&cond);
2.2.3 pthread_cond_wait
函数原型:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
功能:
等待条件变量,并释放互斥锁。具体点来说就是需要等待满足条件才能执行下一步,但是在这一步之前我们可能已经获取了锁,为了避免死锁问题,就需要在等待条件变量时把互斥锁释放掉

参数:

cond:指向条件变量的指针。

mutex:指向互斥锁的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_mutex_lock(&mutex);
while (!ready) {
pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);
2.2.4 pthread_cond_signal
函数原型:

int pthread_cond_signal(pthread_cond_t *cond);
功能:
唤醒一个等待条件变量的线程。

参数:

cond:指向条件变量的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_cond_signal(&cond);
2.2.5 pthread_cond_broadcast
函数原型:

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:
唤醒所有等待条件变量的线程。在使用这种方式时需要注意,因为有些时候当出现多进程的线程时,这种方法可能会导致线程误唤醒,在我们后面讲生产消费模型的时候会讲到这一点

参数:

cond:指向条件变量的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_cond_broadcast(&cond);
2.3 条件变量的使用示例
下面是一个简单的示例,演示了如何使用条件变量来同步两个线程的执行。

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

pthread_mutex_t mutex;
pthread_cond_t cond;
int ready = 0;

void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
while (!ready) {
printf("Thread waiting...\n");
pthread_cond_wait(&cond, &mutex);
}
printf("Thread awakened!\n");
pthread_mutex_unlock(&mutex);
return NULL;
}

int main() {
pthread_t tid;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);

pthread_create(&tid, NULL, thread_func, NULL);

sleep(1); // 模拟一些工作
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

pthread_join(tid, NULL);

pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

return 0;
}

2.4 运行结果
Thread waiting...
Thread awakened!

2.5 代码解析
pthread_cond_wait(&cond, &mutex):线程在等待条件变量时,会释放互斥锁,并在被唤醒后重新获取互斥锁。

pthread_cond_signal(&cond):唤醒一个等待条件变量的线程。

pthread_cond_broadcast(&cond):唤醒所有等待条件变量的线程。

这里的这个代码比较简单,也没有什么现实意义,重要的是先要理解一下条件变量的出现场景和使用,条件变量一般是和互斥锁同时出现的,能够确保在满足条件的情况下才能执行临界区的语句

3. 读写锁
3.1 读写锁的基本概念
读写锁是一种特殊的锁,允许多个线程同时读取共享资源,但在写操作时需要独占访问。读写锁适用于读多写少的场景,可以提高并发性能。

3.2 读写锁的相关函数详解
3.2.1 pthread_rwlock_init
函数原型:

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
功能:
初始化读写锁。

参数:

rwlock:指向读写锁的指针。

attr:读写锁的属性,通常设置为NULL,表示使用默认属性。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
3.2.2 pthread_rwlock_destroy
函数原型:

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:
销毁读写锁。

参数:

rwlock:指向读写锁的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_rwlock_destroy(&rwlock);
3.2.3 pthread_rwlock_rdlock
函数原型:

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:
获取读锁。

参数:

rwlock:指向读写锁的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_rwlock_rdlock(&rwlock);
3.2.4 pthread_rwlock_wrlock
函数原型:

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:
获取写锁。

参数:

rwlock:指向读写锁的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_rwlock_wrlock(&rwlock);
3.2.5 pthread_rwlock_unlock
函数原型:

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:
释放读写锁。

参数:

rwlock:指向读写锁的指针。

返回值:

成功返回0,失败返回错误码。

示例:

pthread_rwlock_unlock(&rwlock);
3.3 读写锁的使用示例
下面是一个简单的示例,演示了如何使用读写锁来保护共享资源。

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

pthread_rwlock_t rwlock;
int shared_data = 0;

void* reader_func(void* arg) {
pthread_rwlock_rdlock(&rwlock);
printf("Reader: shared_data = %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}

void* writer_func(void* arg) {
pthread_rwlock_wrlock(&rwlock);
shared_data++;
printf("Writer: shared_data = %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}

int main() {
pthread_t readers[5], writers[2];
pthread_rwlock_init(&rwlock, NULL);

for (int i = 0; i < 5; i++) {
pthread_create(&readers[i], NULL, reader_func, NULL);
}

for (int i = 0; i < 2; i++) {
pthread_create(&writers[i], NULL, writer_func, NULL);
}

for (int i = 0; i < 5; i++) {
pthread_join(readers[i], NULL);
}

for (int i = 0; i < 2; i++) {
pthread_join(writers[i], NULL);
}

pthread_rwlock_destroy(&rwlock);

return 0;
}

3.4 运行结果
Reader: shared_data = 0
Reader: shared_data = 0
Reader: shared_data = 0
Reader: shared_data = 0
Reader: shared_data = 0
Writer: shared_data = 1
Writer: shared_data = 2

3.5 代码解析
pthread_rwlock_rdlock(&rwlock):获取读锁,允许多个线程同时读取共享资源。

pthread_rwlock_wrlock(&rwlock):获取写锁,独占访问共享资源。

pthread_rwlock_unlock(&rwlock):释放读写锁。

读写锁出现的场景还是比较多的,尤其是当写少读多的时候,读写锁可以帮助我们提高并发效率

4. POSIX信号量
4.1 POSIX信号量的基本概念
POSIX信号量是一种用于线程同步的机制,可以用来控制对共享资源的访问。信号量有一个整数值,表示可用资源的数量。线程可以通过sem_wait和sem_post来分别减少和增加信号量的值。

4.2 POSIX信号量的相关函数详解
4.2.1 sem_init
函数原型:

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
初始化信号量。

参数:

sem:指向信号量的指针。

pshared:指定信号量的类型,0表示线程间共享,非0表示进程间共享。

value:信号量的初始值。

返回值:

成功返回0,失败返回-1。

示例:

sem_t sem;
sem_init(&sem, 0, 0);
4.2.2 sem_destroy
函数原型:

int sem_destroy(sem_t *sem);
功能:
销毁信号量。

参数:

sem:指向信号量的指针。

返回值:

成功返回0,失败返回-1。

示例:

sem_destroy(&sem);
4.2.3 sem_wait
函数原型:

int sem_wait(sem_t *sem);
功能:
减少信号量的值,如果信号量的值为0,则阻塞。

参数:

sem:指向信号量的指针。

返回值:

成功返回0,失败返回-1。

示例:

sem_wait(&sem);
4.2.4 sem_post
函数原型:

int sem_post(sem_t *sem);
功能:
增加信号量的值,并唤醒等待的线程。

参数:

sem:指向信号量的指针。

返回值:

成功返回0,失败返回-1。

示例:

sem_post(&sem);
4.2.5 sem_trywait
函数原型:

int sem_trywait(sem_t *sem);
功能:
尝试减少信号量的值,如果信号量的值为0,则立即返回错误。

参数:

sem:指向信号量的指针。

返回值:

成功返回0,失败返回-1。

示例:

sem_trywait(&sem);
4.2.6 sem_getvalue
函数原型:

int sem_getvalue(sem_t *sem, int *sval);
功能:
获取信号量的当前值。

参数:

sem:指向信号量的指针。

sval:指向存储信号量值的整数的指针。

返回值:

成功返回0,失败返回-1。

示例:

int value;
sem_getvalue(&sem, &value);
4.3 POSIX信号量的使用示例
下面是一个简单的示例,演示了如何使用POSIX信号量来同步两个线程的执行。

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

sem_t sem;

void* thread_func(void* arg) {
printf("Thread waiting...\n");
sem_wait(&sem);
printf("Thread awakened!\n");
return NULL;
}

int main() {
pthread_t tid;
sem_init(&sem, 0, 0);

pthread_create(&tid, NULL, thread_func, NULL);

sleep(1); // 模拟一些工作
printf("Main thread signaling...\n");
sem_post(&sem);

pthread_join(tid, NULL);

sem_destroy(&sem);

return 0;
}

4.4 运行结果
Thread waiting...
Main thread signaling...
Thread awakened!

4.5 代码解析
sem_wait(&sem):减少信号量的值,如果信号量的值为0,则阻塞。

sem_post(&sem):增加信号量的值,并唤醒等待的线程。

我们在前面讲进程的时候就讲过信号量的知识了,这里基本没啥大的差别,关于POSIX信号量的使用,我们在下一章讲生产消费模型的时候也会再详细讲解的,这里大家先理解一下基本用法即可

5. 总结
本文详细讨论了Linux中线程同步的几种机制,包括条件变量、读写锁和POSIX信号量。通过简单的代码示例,我们演示了如何使用这些机制来同步多个线程的执行。在实际的多线程编程中,选择合适的同步机制非常重要,可以有效避免数据竞争、死锁等问题。

5.1 条件变量的适用场景
条件变量适用于需要等待某个条件成立的场景,通常与互斥锁一起使用。例如,生产者-消费者模型中的缓冲区满或空的情况。

5.2 读写锁的适用场景
读写锁适用于读多写少的场景,可以提高并发性能。例如,数据库系统中的读操作远多于写操作。

5.3 POSIX信号量的适用场景
POSIX信号量适用于需要控制对共享资源访问的场景。例如,限制同时访问某个资源的线程数量。

5.4 选择同步机制的考虑因素
在选择线程同步机制时,需要考虑以下因素:

性能:不同的同步机制对性能的影响不同,需要根据具体场景选择最合适的机制。

复杂性:某些同步机制的使用较为复杂,需要仔细设计和测试。

可维护性:选择易于理解和维护的同步机制,可以减少代码的复杂性和出错的可能性。

总之,线程的同步是确保线程安全很重要的一步,今天我们讲解了确保线程同步机制的重要的几个方面,关于这几个方面的知识点的应用在下一篇生成消费模型还会仔细讲解一遍
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/2301_80220607/article/details/145843271

阅读剩余
THE END