什么是竟态条件和锁?
竟态条件:当多个线程并发访问和修改同一个共享资源(如全局变量)时,如果没有适当的同步措施,就会遇到线程同步问题,这种情况下,程序最终的结果依赖于线程执行的具体时序,导致了竟态条件。
竟态条件是一种特定的线程同步问题,指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序,它会导致程序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机,为了确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竟态条件。
如何避免竟态条件呢?
(1)避免多线程写入一个地址
(2)给资源加锁,使同一时间操作特定资源的线程只有一个。
常见的锁机制?
锁主要用于互斥,即在同一时间只允许一个执行单元(进程或线程)访问共享资源。常见的锁机制共有三种:
(1)互斥锁(Mutex):保证同一时刻只有一个线程可以执行临界区的代码。
(2)读写锁(Reader/Writer Locks):允许多个读者同时读取共享数据,但写者的访问是互斥的
(3)自旋锁(Spinlocks):在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景,一般是Linux内核使用。
一,互斥锁
------------------------mutex_test.c------------------------
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define THREAD_COUNT 20000
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
/**
* @brief 对传入值累加1
* @param argv 传入指针
* @return void* 无返回值
*/
void *add_thread(void *argv)
{
int *p = argv;
// 累加之前加锁,此时其他获取该锁的线程都会被阻塞
pthread_mutex_lock(&counter_mutex);
(*p)++;
// 累加之后释放锁
pthread_mutex_unlock(&counter_mutex);
return (void *)0;
}
int main()
{
pthread_t pid[THREAD_COUNT];
int num = 0;
// 用20000个线程对num作累加
for (int i = 0; i < THREAD_COUNT; i++)
{
pthread_create(pid + i, NULL, add_thread, &num);
}
// 等带所有线程结束
for (int i = 0; i < THREAD_COUNT; i++)
{
pthread_join(pid[i], NULL);
}
// 打印累加结果
printf("累加结果:%d\n", num);
return 0;
}
| 函数 | 作用 | 原型 | 示例 |
|---|---|---|---|
| 1. 初始化 | 初始化互斥锁 | int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); |
pthread_mutex_init(&mtx, NULL); |
| 2. 加锁 | 尝试获取锁(阻塞) | int pthread_mutex_lock(pthread_mutex_t *mutex); |
pthread_mutex_lock(&mtx); |
| 3. 解锁 | 释放锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); |
pthread_mutex_unlock(&mtx); |
| 4. 销毁 | 释放锁资源(仅动态初始化时需要) | int pthread_mutex_destroy(pthread_mutex_t *mutex); |
pthread_mutex_destroy(&mtx); |
| 5. 静态初始化 | 编译期初始化(推荐用于全局/静态变量) | PTHREAD_MUTEX_INITIALIZER(宏) |
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; |
静态初始化 + lock/unlock 是互斥锁最常用模式;
在上述代码中,互斥锁counter_mutex并未被显示销毁,但这通常不会引起资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存,打开的文件描述符和互斥锁等,因此即便没有显示销毁互斥锁也不会有问题,
但在某些情况下,确实需要显示销毁互斥锁资源,如果互斥锁时动态分配的(使用pthread_mutex_init函数初始化),或者互斥锁会被跨多个函数或文件使用,不需要时必须显式销毁它,但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程序中的counter_mutex),显式销毁不是必须的。
那么静态初始化跟动态初始化有什么区别呢?
静态初始化:
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
简洁,适合全局/静态变量,只能使用默认属性;不能用于堆分配的变量(如 malloc 出来的结构体成员),不能销毁 (pthread_mutex_destroy 会导致未定义行为)
动态初始化:
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// ... 使用 ...
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
可设置自定义属性 (如 mutex 类型、cond 的进程共享);可用于任何生命周期的变量 (栈、堆、全局);可显式销毁 ,释放资源;适用场景:需要灵活控制、或变量非全局时。
条件变量:pthread_cond_t
作用是让线程安全地睡眠,直到某个条件成立(如"缓冲区不为空")。必须与互斥锁配合使用
pthread_cond_wait(&cond, &mutex):原子地释放锁 + 睡眠pthread_cond_signal(&cond):唤醒一个等待线程pthread_cond_broadcast(&cond):唤醒所有等待线程
------------------------condition_var.c------------------------
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
// 初始化锁
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 生产者线程
void *producer(void *arg)
{
int item = 1;
while (1)
{
// 获取互斥锁 拿不到等
pthread_mutex_lock(&mutex);
// 如果缓冲区满,等消费者读取
while (count == BUFFER_SIZE)
{
// 暂停线程 等待唤醒
pthread_cond_wait(&cond, &mutex); // 缓冲区满 → 睡觉,释放锁
}
// 能到这里说明缓冲区不满了 可以写一个
buffer[count++] = item++; // 生产
printf("白月光发送一个幸运数字%d\n", buffer[count - 1]);
// 通知消费者可以消费数据了
// 唤醒消费者 同时解锁
pthread_cond_signal(&cond); // 唤醒一个消费者
pthread_mutex_unlock(&mutex);
}
}
void *consumer(void *arg)
{
while (1)
{
// 获取互斥锁 拿不到等
pthread_mutex_lock(&mutex);
// 如果缓冲区为空,则等待生产者生产数据
while (count == 0)
{
// 暂停线程 等待唤醒
pthread_cond_wait(&cond, &mutex); // 缓冲区空 → 睡觉,释放锁
}
printf("我收到了幸运数字 %d\n", buffer[--count]);
// 通知生产者可以发送数据了
// 唤醒生产者 同时解锁
pthread_cond_signal(&cond); // 唤醒一个生产者
pthread_mutex_unlock(&mutex);
}
}
int main()
{
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);
return 0;
}
---------------------------部分日志运行内容------------------------
白月光发送一个幸运数字143737
白月光发送一个幸运数字143738
白月光发送一个幸运数字143739
白月光发送一个幸运数字143740
白月光发送一个幸运数字143741
我收到了幸运数字 143741
我收到了幸运数字 143740
我收到了幸运数字 143739
我收到了幸运数字 143738
我收到了幸运数字 143737
白月光发送一个幸运数字143742
白月光发送一个幸运数字143743
白月光发送一个幸运数字143744
白月光发送一个幸运数字143745
白月光发送一个幸运数字143746
我收到了幸运数字 143746
我收到了幸运数字 143745
我收到了幸运数字 143744
我收到了幸运数字 143743
我收到了幸运数字 143742
pthread_cond_wait(&cond, &mutex)这一行代码做了三件事:
1,释放互斥锁mutex -> 允许其他线程(如生产者)进入临界区;
2,将当前线程加入cond的等待队列;
3,睡眠,直到被signal / broadcast唤醒;
4,被唤醒后,自动重新获取mutex(可能再次阻塞)。
| 对比项 | 只用互斥锁 | 互斥锁 + 条件变量 |
|---|---|---|
| 等待方式 | 忙等待(循环+usleep) | 睡眠等待(零 CPU 占用) |
| 效率 | 浪费 CPU 资源 | 高效,线程真正睡眠 |
| 响应性 | 延迟高(靠 sleep 间隔) | 实时唤醒(signal 立即触发) |
| 可扩展性 | 难以支持复杂条件 | 支持任意条件等待 |
| 正确性 | 易出错(竞态条件) | POSIX 标准同步原语,安全 |
二,读写锁
------------------------rwlock_test.c------------------------
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t rwlock;
int shared_data = 0;
void *lock_reader(void *argv)
{
pthread_rwlock_rdlock(&rwlock);
printf("this is %s, value is %d\n", (char *)argv, shared_data);
sleep(1);
pthread_rwlock_unlock(&rwlock);
}
void *lock_writer(void *argv)
{
pthread_rwlock_wrlock(&rwlock);
int tmp = shared_data + 1;
shared_data = tmp;
printf("this is %s, shared_data++\n", (char *)argv);
pthread_rwlock_unlock(&rwlock);
}
int main()
{
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
// 设置写优先
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
pthread_t writer1, writer2, reader1, reader2, reader3, reader4, reader5, reader6;
pthread_create(&writer1, NULL, lock_writer, "writer1");
pthread_create(&reader1, NULL, lock_reader, "reader1");
pthread_create(&reader2, NULL, lock_reader, "reader2");
pthread_create(&reader3, NULL, lock_reader, "reader3");
pthread_create(&writer2, NULL, lock_writer, "writer2");
pthread_create(&reader4, NULL, lock_reader, "reader4");
pthread_create(&reader5, NULL, lock_reader, "reader5");
pthread_create(&reader6, NULL, lock_reader, "reader6");
pthread_join(writer1, NULL);
pthread_join(writer2, NULL);
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(reader3, NULL);
pthread_join(reader4, NULL);
pthread_join(reader5, NULL);
pthread_join(reader6, NULL);
pthread_rwlock_destroy(&rwlock);
}
------------------日志内容------------------
this is writer1, shared_data++
this is reader1, value is 1
this is reader2, value is 1
this is reader3, value is 1
this is writer2, shared_data++
this is reader5, value is 2
this is reader4, value is 2
this is reader6, value is 2
| 函数 | 作用 |
|---|---|
pthread_rwlockattr_init |
初始化读写锁属性对象 |
pthread_rwlockattr_setkind_np |
Linux 特有:设置读写锁类型(如写者优先) |
pthread_rwlock_init |
根据属性初始化读写锁 |
pthread_rwlockattr_destroy |
销毁读写锁属性对象 |
pthread_rwlock_rdlock |
获取读锁(共享) |
pthread_rwlock_wrlock |
获取写锁(独占) |
pthread_rwlock_unlock |
释放读锁或写锁 |
pthread_rwlock_destroy |
销毁读写锁 |
读操作:在读写锁地控制下,多个线程可以同时获得读锁,这些线程可以并发的读取共享资源,但他们的存在阻止了写锁地授予。
写操作:如果至少有一个读操作持有读锁,写操作就无法获得写锁,写操作将会阻塞,直到所有的读锁都被释放。
在这套代码中我们可以看到使用了两个写加六个读来模拟多线程混合并发,我们用pthread_rwlockattr_t attr来定义了pthread_rwlockattr_init(&attr)所需要的属性地址attr,然后用pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);来设置了写者优先,这是linux特有的,没有移植性,但实际上只是有可能的缓解了写饥饿(写饥饿就是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁而不断的推迟写线程的执行),然后再调用pthread_rwlockattr_destroy(&attr);销毁读写锁属性对象,
// 1. 声明一个读写锁变量(尚未初始化)
pthread_rwlock_t rwlock;
// 2. 声明一个读写锁属性对象(用于配置锁的行为)
pthread_rwlockattr_t attr;
// 3. 初始化属性对象为默认值
pthread_rwlockattr_init(&attr);
// 4. (可选)设置特定属性:此处尝试启用"写者优先"策略(Linux 扩展)
pthread_rwlockattr_setkind_np(&attr,
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
// 5. 使用配置好的属性对象初始化读写锁
pthread_rwlock_init(&rwlock, &attr); // ← 属性在此刻生效
// 6. 销毁属性对象(释放其内部资源)
pthread_rwlockattr_destroy(&attr); // ← 立即可安全销毁!
// 7. (后续)正常使用读写锁:加锁、临界区、解锁
// ... 使用 rwlock ...
// 8. 程序结束前销毁读写锁(释放其内部资源)
pthread_rwlock_destroy(&rwlock);
三,自旋锁
在Linux内核中,自旋锁是一种用于多处理器系统中获得低级同步机制,主要用于保护非常短的代码断货数据结构,以避免多个处理器同时访问共享资源,自旋锁相对于其他锁的优点时它们在锁被占用时会持续检查所得状态(即"自旋"),而不是让线程进入休眠,这使得自旋锁在等待时间非常短的情况下非常有效,因为他避免了线程上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销,不能在用户空间使用。总结一下就是就是系统内核会用到,为了避免上下文的切换,让他不断地进行一个死循环