Linux —— 线程同步 - 条件变量
- 条件变量的概念
- 互斥量与条件变量的关系
- 条件变量的操作
- 代码示例
条件变量的概念
条件变量是一种用于线程间同步的机制,主要用于协调线程之间的执行顺序,允许线程在某个条件不满足时进入等待状态,直到其他线程通知条件已满足。条件变量通常与互斥锁一起使用,以确保在检查和修改共享状态时的安全性。
主要功能:
- 线程等待:当某个条件不满足时,线程可以在条件变量上等待。
- 线程通知:当条件满足时,其他线程可以通知等待的线程继续执行。
条件变量的核心作用是提供一种机制,使得线程可以在条件未满足时挂起自己,并在条件满足时被唤醒。
这样可以避免忙等待(busy waiting)和不必要的 CPU 资源浪费。
互斥量与条件变量的关系
互斥量和条件变量在多线程编程中密切相关,通常结合使用以实现更加复杂的同步机制。他们之间的关系可以概括为一下几点:
-
互斥量保护条件变量
条件变量的操作必须在互斥量的保护之下进行,以确保对共享资源的安全访问。线程在调用pthread_cond_wait()
等待条件变量时,必须先获取相关的互斥变量锁。这样可以防止其他线程在条件检查和修改期间访问共享资源。(对共享区资源的检查也是一种访问) -
等待与通知机制
当线程调用pthread_cond_wait()
来进入等待状态时,互斥量会被自动解锁,允许其他线程修改共享资源和条件状态。其他线程在改变条件后,会使用pthread_cond_signal()
和pthread_cond_broadcast()
来通知等待的线程,这需要在持有互斥量的情况下使用。 -
重新检查条件
被唤醒的线程在继续执行之前,必须重新获取互斥量并检查条件是否满足。这是因为在它被唤醒
之前,其他线程可能已经改变了条件状态。线程不能假设条件一定满足,而是需要重新验证。 -
互斥量的状态影响条件变量的使用
互斥量的状态(锁定或解锁)直接影响到线程是否能够在条件变量上等待或被唤醒。只有在互斥
量被解锁的情况下,其他线程才能访问和修改条件变量的相关条件。 -
组合使用
互斥量和条件变量通常一起使用来解决复杂的同步问题,例如生产者-消费者问题、读者-写者问
题等。它们的组合使用能够有效地协调线程之间的执行顺序,避免竞争条件和资源浪费。
总之,互斥量和条件变量是多线程编程中不可或缺的同步机制。互斥量确保同一时刻只有一个线
程可以访问共享资源,而条件变量则提供了一种线程等待和通知的机制。通过将这两种机制结合
使用,程序员可以编写出更加复杂和高效的多线程程序。
关键点总结:
- 互斥量的自动解锁:当线程在条件变量上等待时,互斥量会被自动解锁,这使得其他线程可以修改共享资源。
- 条件的重新检查:被唤醒的线程在继续执行之前必须重新获取互斥量并检查条件,以确保条件的有效性。
- 互斥量的状态影响条件变量的使用:互斥量的状态(锁定或解锁)直接影响到线程是否能够在条件变量上等待或被唤醒。
条件变量的操作
条件变量的操作主要包括:
- 等待(Wait):当线程发现条件不满足时,它会调用
pthread_cond_wait()
函数进入等待状态。在调用该函数时,线程会自动释放与条件变量关联的互斥量,并进入阻塞状态,等待其他线程的通知。 - 通知(Signal):当条件满足时,其他线程可以调用
pthread_cond_signal()
或pthread_cond_broadcast()
来唤醒一个或多个等待该条件变量的线程。
1.条件变量的初始化:
函数原型:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数
cond
: 指向要初始化的条件变量的指针。attr
: 指向条件变量属性对象的指针。如果设置为 NULL,则使用默认属性。
返回值
- 成功时返回 0。
- 失败时返回错误编号,例如:
EINVAL
: 参数无效。ENOMEM
: 内存不足。
2.条件变量的销毁
函数原型:
int pthread_cond_destroy(pthread_cond_t *cond);
参数
cond
: 指向要销毁的条件变量的指针。
返回值
- 成功时返回 0。
- 失败时返回错误编号,例如:
EBUSY
: 条件变量上仍有线程在等待。EINVAL
: 参数无效。
- 等待条件变量
函数原型:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数
cond
: 指向条件变量的指针。mutex
: 指向与条件变量关联的互斥量的指针。
返回值- 成功时返回 0。
- 失败时返回错误编号,例如:
EINVAL
: 参数无效。ESRCH
: 线程不存在。
为什么要在pthread_cond_wait中传入互斥锁?
在
pthread_cond_wait()
中传入互斥锁是为了确保线程安全。当线程调用该函数时,互斥锁会被自动解锁,允许其他线程访问和修改共享资源。线程在等待条件变量时,必须持有互斥锁,以防止竞争条件,并在被唤醒后重新获取互斥锁,以确保对共享资源的安全访问。
- 唤醒条件变量
函数原型:
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
参数
- cond: 指向条件变量的指针。
返回值
- 成功时返回 0。
- 失败时返回错误编号,例如:
- EINVAL: 参数无效。
使用说明:
pthread_cond_signal()
:唤醒一个等待该条件变量的线程。如果没有线程在等待,则不执行任何操作。pthread_cond_broadcast()
:唤醒所有等待该条件变量的线程。
代码示例
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
const int num = 2;
int n = 4;
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
void* Wait(void* args)
{
std::string name = *static_cast<std::string*>(args);
delete static_cast<std::string*>(args); // 释放动态分配的内存
int m = 2;
while (m--)
{
pthread_mutex_lock(&g_mutex);
pthread_cond_wait(&g_cond, &g_mutex);
std::cout << "I am: " << name << " ..." << std::endl;
pthread_mutex_unlock(&g_mutex);
}
return nullptr;
}
int main()
{
pthread_t threads[num];
for (int i = 0; i < num; i++)
{
auto name = new std::string("thread-" + std::to_string(i + 1));
if (pthread_create(&threads[i], nullptr, Wait, name) != 0) {
std::cerr << "Failed to create thread " << i + 1 << std::endl;
delete name; // 确保内存被释放
return 1;
}
}
sleep(1);
while (n--)
{
pthread_mutex_lock(&g_mutex); // 锁住互斥量
pthread_cond_signal(&g_cond);
std::cout << "唤醒了一个线程" << std::endl;
pthread_mutex_unlock(&g_mutex); // 解锁互斥量
sleep(2);
}
for (int i = 0; i < num; i++)
{
pthread_join(threads[i], nullptr);
}
// 销毁互斥量和条件变量
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_cond);
return 0;
}
这段代码实现了一个简单的多线程程序,其中创建了两个线程,它们在条件变量上等待,直到主线程发出信号唤醒它们。代码使用了互斥量来确保对共享资源的安全访问,并使用条件变量来实现线程之间的同步。
-
在 main 函数中,首先创建了两个线程,并指定它们要执行的函数为
Wait
。每个线程都有一个唯一的名称,用于标识它们。 -
创建线程时,使用了
new
动态分配内存来存储线程名称。主线程在创建所有线程后睡眠 1 秒钟,以确保所有线程都已创建并处于等待状态。 -
然后,主线程进入循环,共执行 4 次。在每次循环中,它都会调用
pthread_cond_signal
函数来唤醒一个等待的线程。在唤醒线程之后,主线程会打印一条消息,表示已经唤醒了一个线程,并睡眠 2 秒钟。 -
在主线程唤醒所有线程之后,它会等待所有线程完成执行。这是通过调用
pthread_join
函数来实现的。当所有线程都完成执行后,main 函数返回 0,表示程序成功退出。 -
在
Wait
函数中,每个线程都会尝试获取互斥量g_mutex
。一旦获取到互斥量,线程就会调用pthread_cond_wait
函数在条件变量g_cond
上等待。
这里的关键点是:- 获取互斥量:
- 在进入
while
循环之前,线程会调用pthread_mutex_lock(&g_mutex)
来获取互斥量g_mutex
。 - 这确保了在访问共享资源和条件变量时,只有一个线程可以进入临界区。
- 在进入
- 等待条件变量:
- 获取互斥量后,线程会立即调用
pthread_cond_wait(&g_cond, &g_mutex)
。 pthread_cond_wait()
函数有两个参数:条件变量g_cond
和互斥量g_mutex
。- 当线程调用这个函数时,会发生以下事情:
- 线程会自动释放互斥量
g_mutex
。这允许其他线程在此期间获取互斥量并进入临界区。 - 线程会被放入条件变量
g_cond
的等待队列,并进入阻塞状态。
- 线程会自动释放互斥量
- 获取互斥量后,线程会立即调用
- 被唤醒后:
- 当其他线程调用
pthread_cond_signal()
或pthread_cond_broadcast()
来唤醒等待的线程时,被唤醒的线程会重新获取互斥量g_mutex
。 - 这确保了在继续执行之前,线程可以安全地访问共享资源。
- 当其他线程调用
所以,在 Wait 函数中,每个线程首先获取互斥量,然后调用
pthread_cond_wait()
在条件变量上等待。这个过程确保了线程在访问共享资源之前拥有互斥量,并且只有在条件满足时才会继续执行。 - 获取互斥量:
-
在调用
pthread_cond_wait
时,线程会自动释放互斥量g_mutex
。当线程被唤醒时,它会重新获取互斥量g_mutex
。一旦获取到互斥量,线程就会打印一条消息,表示它已经被唤醒。在打印消息之后,线程会释放互斥量g_mutex
。这个过程会重复两次。