文章目录
- 1.死锁
- 1.1概念
- 1.2死锁的必要条件
- 2.线程同步相关接口
- 2.1pthread_cond_init/destroy()
- 2.2int pthread_cond_wait
- 2. 3linux下的条件变量及其作用
- 2.4int pthread_cond_signal/broadcast();
- 2.5Linux下 阻塞和挂起的异同
- 2.6阻塞,挂起,和进程切换的关系
- 3.由浅入深理解线程同步
- 使用接口 代码测试
- 总结线程同步/条件变量
1.死锁
1.1概念
死锁(Deadlock)是指在并发系统中,两个或多个进程(或线程)因为互相等待对方释放锁资源而无法继续执行的状态。在死锁状态下,进程无法前进,也无法释放资源,导致系统无法正常运行。死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
死锁通常发生在多个进程(或线程)同时竞争有限的资源时,每个进程都在等待其他进程释放资源,而自己又无法释放已经占有的资源。一个执行流,一把互斥锁也可能导致死锁,即加锁后,不解锁,再次申请锁。
1.2死锁的必要条件
互斥条件:通过加锁使得一个资源每次只能被一个执行流使用(不加锁自然就不会产生死锁)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持占有。
不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能对其强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系,使得每个执行流都在等待下一个执行流所占有的资源。
当这四个条件同时满足时,就可能发生死锁。一旦发生死锁,系统将无法自动解除死锁状态,需要通过人工干预来解决。
避免死锁
破坏互斥条件:一个资源允许被多个执行流使用
破坏请求与保持条件:要求进程(或线程)在执行之前一次性获取所有需要的资源,否则在等待资源时释放已经占有的资源。
破坏不可剥夺条件:允许系统强制剥夺某些进程(或线程)已获得的未使用完的资源。
破坏循环等待条件:通过对资源进行排序,按照固定的顺序申请资源,避免交叉申请,循环等待。【加锁顺序一致】
避免锁未释放
精简临界区代码,缩短持有锁的时间
合并临界区,资源一次性分配
死锁是并发系统中的一个重要问题,对系统的性能和可靠性有很大影响。因此,在设计和实现并发系统时,需要合理地分配和管理资源,以避免死锁的发生。
避免死锁算法
死锁检测算法
银行家算法
Linux下pthread_mutex_trylock和pthread_mutex_lock函数有什么异同
pthread_mutex_lock 和 pthread_mutex_trylock 都是 POSIX 线程(Pthreads)库中的函数,用于操作互斥锁(mutex)。这两个函数的主要目的是保护共享资源,防止多个线程同时访问和修改这些资源,从而引发数据竞争和不一致的问题。
下面是这两个函数的异同点:
相同点:
用途:两者都用于获取互斥锁,以实现对共享资源的同步访问。
阻塞特性:如果互斥锁当前被其他线程持有,pthread_mutex_lock 会阻塞调用线程,直到锁被释放并成功获取。
不同点:
阻塞行为:
pthread_mutex_lock:如果互斥锁当前被其他线程持有,调用线程会阻塞,直到锁变得可用。
pthread_mutex_trylock:如果互斥锁当前被其他线程持有,调用线程不会阻塞,而是立即返回错误(通常是 EBUSY)。
返回值:
pthread_mutex_lock:成功时返回 0,失败时返回错误码。
pthread_mutex_trylock:成功获取锁时返回 0,锁已被其他线程持有时返回 EBUSY,其他错误情况返回相应的错误码。
适用场景:
pthread_mutex_lock:适用于需要确保线程在继续执行前能够获取到锁的情况。在大多数同步场景中,这是首选方法。
pthread_mutex_trylock:适用于不希望阻塞的情况,或者用于在尝试获取锁之前执行其他操作的场景。例如,你可能想在尝试获取锁之前检查某个条件,或者希望在无法获取锁时执行某些后备操作。
示例:
使用 pthread_mutex_lock 的示例:
c
pthread_mutex_t mutex;
// 初始化互斥锁...
// 尝试获取锁
if (pthread_mutex_lock(&mutex) != 0) {
// 处理错误...
}
// 访问共享资源...
// 释放锁
pthread_mutex_unlock(&mutex);
使用 pthread_mutex_trylock 的示例:
c
pthread_mutex_t mutex;
// 初始化互斥锁...
// 尝试非阻塞地获取锁
if (pthread_mutex_trylock(&mutex) == EBUSY) {
// 锁已被其他线程持有,执行其他操作或等待...
} else if (pthread_mutex_trylock(&mutex) != 0) {
// 处理其他错误...
} else {
// 成功获取锁,访问共享资源...
pthread_mutex_unlock(&mutex);
}
总的来说,pthread_mutex_lock 和 pthread_mutex_trylock 在功能上是相似的,但在处理互斥锁不可用时的行为上有所不同。选择哪个函数取决于你的具体需求和你希望如何处理锁不可用的情况。
如何理解Linux下的线程同步?线程同步有什么意义?
在Linux下,线程同步是指控制多线程间的相对执行顺序,从而在线程间正确、有序地共享数据。线程同步的目的是确保线程在访问共享资源时不会发生冲突,维护数据的一致性和完整性。
具体来说,线程同步涉及一个线程在发出某一功能调用时,在没有得到结果前,该调用不会返回。同时,其他线程为保证数据的一致性,也不能调用该功能。这样做可以避免多线程同时访问和修改共享资源时可能出现的竞态条件,确保共享资源的正确访问顺序。
线程同步的意义在于:
避免数据不一致和错误:当多个线程并发访问和修改共享资源时,如果没有适当的同步机制,就可能导致数据不一致和错误。线程同步可以确保同一时间只有一个线程访问和修改共享资源,从而避免这种情况。
提高程序的可预测性和稳定性:通过线程同步,可以确保线程按照预定的顺序执行,使程序的行为更加可预测。同时,也可以减少由于并发访问导致的冲突和错误,提高程序的稳定性。
优化资源利用:适当的线程同步可以确保资源在多个线程之间得到合理分配和利用,避免资源的浪费和冲突。
在Linux中,实现线程同步的常见方法包括使用互斥锁、条件变量、读写锁等。这些机制可以帮助开发者在编写多线程程序时,有效地管理线程间的同步和互斥关系,确保程序的正确性和性能。
2.线程同步相关接口
2.1pthread_cond_init/destroy()
int pthread_cond_init(
pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr
);
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_init 和 pthread_cond_destroy 是 POSIX 线程库中的函数,它们用于初始化和销毁条件变量。这两个函数在多线程编程中非常重要,因为它们允许线程在特定条件满足之前进行等待。
- pthread_cond_init
函数原型:
c
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数:
cond: 指向要初始化的条件变量的指针。
attr: 指向条件变量属性的指针,通常设置为 NULL 以使用默认属性。
返回值:
如果成功,返回 0。
如果失败,返回错误号。
工作原理:
这个函数会初始化一个条件变量。条件变量通常与互斥锁(mutex)一起使用,以允许线程在特定条件不满足时等待。
作用:
初始化一个条件变量,以便后续可以在其上执行 pthread_cond_wait、pthread_cond_signal 或 pthread_cond_broadcast 等操作。
- pthread_cond_destroy
函数原型:
c
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond: 指向要销毁的条件变量的指针。
返回值:
如果成功,返回 0。
如果失败,返回错误号。
工作原理:
这个函数会销毁一个之前通过 pthread_cond_init 初始化的条件变量。销毁条件变量会释放与之关联的任何系统资源。
作用:
当不再需要某个条件变量时,应使用此函数来销毁它,以释放相关资源。
示例用法
这里是一个简单的示例,展示了如何使用 pthread_cond_init 和 pthread_cond_destroy:
c
#include <pthread.h>
#include <stdio.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
int shared_data = 0;
void *thread_function(void *arg) {
pthread_mutex_lock(&mutex);
while (shared_data == 0) {
pthread_cond_wait(&cond, &mutex);
}
printf("Thread woke up and shared_data is %d\n", shared_data);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t thread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&thread, NULL, thread_function, NULL);
// Do some work...
sleep(1); // Simulate work by sleeping for a second
pthread_mutex_lock(&mutex);
shared_data = 1;
pthread_cond_signal(&cond); // Signal the waiting thread
pthread_mutex_unlock(&mutex);
pthread_join(thread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
在这个示例中,我们创建了一个线程,该线程等待 shared_data 变量变为非零值。主线程在模拟完成一些工作后,会更改 shared_data 的值,并通过 pthread_cond_signal 通知等待的线程。这样,等待的线程就会醒来并继续执行。最后,我们使用 pthread_mutex_destroy 和 pthread_cond_destroy 来销毁互斥锁和条件变量。
2.2int pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
pthread_cond_wait 是 POSIX 线程库中的一个函数,用于线程同步。它允许一个线程等待一个特定的条件成立,同时释放它持有的互斥锁,使其他线程有机会修改条件。一旦条件成立,等待的线程将被唤醒并重新获取互斥锁,然后继续执行。
函数参数
pthread_cond_t *restrict cond: 指向条件变量的指针。这个条件变量用于线程间的同步。
pthread_mutex_t *restrict mutex: 指向互斥锁的指针。这个互斥锁必须在调用 pthread_cond_wait 之前被当前线程锁定。调用 pthread_cond_wait 时,互斥锁会自动被释放,允许其他线程修改条件。当 pthread_cond_wait 返回时,互斥锁会再次被当前线程锁定。
返回值
如果成功,函数返回 0。
如果出现错误,函数返回错误码。
工作原理
释放互斥锁:调用 pthread_cond_wait 的线程首先会释放它之前持有的互斥锁。这一步是必要的,以确保其他线程可以获取互斥锁并修改条件。
等待条件成立:线程进入等待状态,等待条件变量的信号。在此期间,线程不会执行任何指令,也不会消耗 CPU 时间。
重新获取互斥锁:当条件变量的信号被发送(通过 pthread_cond_signal 或 pthread_cond_broadcast),等待的线程会被唤醒,并尝试重新获取之前释放的互斥锁。一旦获取到互斥锁,线程就可以继续执行。
作用
pthread_cond_wait 的主要作用是允许线程在条件不满足时安全地等待,同时释放它持有的资源(这里是互斥锁),以便其他线程可以修改条件。这种机制在多线程编程中非常有用,尤其是在实现生产者-消费者模型、信号量、以及其他需要线程间同步和协调的场景中。
示例
下面是一个简单的示例,展示了 pthread_cond_wait 的用法:
c
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int shared_data = 0;
void *thread_function(void *arg) {
pthread_mutex_lock(&mutex);
while (shared_data == 0) {
printf("Thread waiting for shared_data to become non-zero...\n");
pthread_cond_wait(&cond, &mutex); // 等待条件成立,同时释放互斥锁
}
printf("Thread woke up and shared_data is %d\n", shared_data);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
// 模拟一些工作,然后设置共享数据并发送条件信号
sleep(1);
pthread_mutex_lock(&mutex);
shared_data = 1;
printf("Main thread setting shared_data to 1 and signaling the condition...\n");
pthread_cond_signal(&cond); // 发送条件信号,唤醒等待的线程
pthread_mutex_unlock(&mutex);
pthread_join(thread, NULL);
return 0;
}
在这个示例中,一个线程等待 shared_data 变为非零值,而主线程在模拟完成一些工作后设置 shared_data 的值并发送条件信号。通过 pthread_cond_wait,等待的线程可以在条件不满足时安全地等待,并在条件成立时被唤醒。
2. 3linux下的条件变量及其作用
在Linux系统中,条件变量(Condition Variable)是一种重要的同步机制,它主要用于实现多线程程序中的线程间通信和同步。条件变量通常与互斥锁(Mutex)一起使用,以确保线程在访问共享资源时的安全性和一致性。
条件变量的主要作用是允许一个线程等待另一个线程满足某个条件后再继续执行。当一个线程发现某个条件不满足时,它可以通过条件变量进行等待,而另一个线程在满足条件时可以通过条件变量通知等待的线程。这种机制可以有效地实现线程间的协同工作,避免线程间的竞争和冲突。
具体来说,条件变量的工作原理如下:当一个线程需要等待某个条件成立时,它会首先获取互斥锁,然后检查条件是否满足。如果条件不满足,线程会调用pthread_cond_wait函数将自身挂起,并释放互斥锁,进入等待状态。此时,其他线程可以获取互斥锁并执行相关操作,以改变条件的状态。当条件满足时,另一个线程会调用pthread_cond_signal或pthread_cond_broadcast函数来通知等待的线程。收到通知的线程会重新获取互斥锁,并重新检查条件。如果条件仍然满足,线程会继续执行后续操作;否则,它会再次进入等待状态。
通过条件变量,我们可以实现复杂的线程同步逻辑,如生产者-消费者模型、线程池等。这些模式在并发编程中非常常见,它们能够有效地利用系统资源,提高程序的性能和响应速度。
总之,Linux下的条件变量是一种强大的线程同步工具,它可以帮助开发者在多线程环境中实现线程间的有序协作和高效通信。
2.4int pthread_cond_signal/broadcast();
pthread_cond_broadcast 和 pthread_cond_signal 是 POSIX 线程库中的两个函数,用于唤醒等待在特定条件变量上的线程。这两个函数都是条件变量同步机制的一部分,常用于多线程编程中,以实现线程间的协调与同步。
函数参数
pthread_cond_t *cond: 指向条件变量的指针。这两个函数都通过这个指针来操作特定的条件变量。
返回值
如果函数执行成功,返回值为 0。
如果出现错误,比如传入的条件变量指针为 NULL,或者其它系统级错误,会返回错误码。
工作原理
pthread_cond_signal:这个函数会唤醒等待在指定条件变量上的单个线程。如果有多个线程在等待,那么具体唤醒哪一个线程是由系统调度器决定的,通常是按照等待的顺序来唤醒。
pthread_cond_broadcast:这个函数会唤醒等待在指定条件变量上的所有线程。所有等待的线程都会被标记为可运行状态,但具体哪个线程会首先得到 CPU 执行权,还是由系统调度器决定。
在这两个函数被调用时,被唤醒的线程会重新尝试获取它们之前释放的互斥锁(如果它们之前是通过 pthread_cond_wait 进入等待状态的)。当它们成功获取到互斥锁后,就可以继续执行了。
作用
线程同步与协调:条件变量常常与互斥锁一起使用,以在多线程环境中实现同步和协调。pthread_cond_signal 和 pthread_cond_broadcast 允许一个线程通知其他线程某个条件已经满足,从而允许那些线程继续执行。
避免忙等待:通过使用条件变量,线程可以在条件不满足时进入等待状态,而不是忙等待(即不断循环检查条件是否满足)。这可以提高系统的效率和响应性。
实现复杂的同步模式:通过使用 pthread_cond_signal 和 pthread_cond_broadcast,可以实现各种复杂的线程同步模式,如生产者-消费者模型、读者-写者问题等。
示例
下面是一个简单的示例,展示了 pthread_cond_signal 的用法:
c
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
void *worker_thread(void *arg) {
pthread_mutex_lock(&mutex);
while (!ready) {
printf("Worker thread waiting...\n");
pthread_cond_wait(&cond, &mutex); // 等待条件成立
}
printf("Worker thread woke up and ready is %d\n", ready);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, worker_thread, NULL);
// 做一些工作,然后设置 ready 变量并发送信号
sleep(1);
pthread_mutex_lock(&mutex);
ready = 1;
printf("Main thread setting ready to 1 and signaling the condition...\n");
pthread_cond_signal(&cond); // 发送信号,唤醒等待的线程
pthread_mutex_unlock(&mutex);
pthread_join(thread, NULL);
return 0;
}
在这个示例中,一个工作线程等待 ready 变量变为 1,而主线程在模拟完成一些工作后设置 ready 的值,并通过 pthread_cond_signal 发送信号来唤醒等待的工作线程。
2.5Linux下 阻塞和挂起的异同
在Linux系统中,阻塞和挂起都是进程管理的重要概念,用于控制和管理进程的执行状态。尽管它们都涉及进程的暂停执行,但它们在实现方式、目的和恢复机制等方面存在显著的差异。
首先,阻塞(Blocking)通常发生在进程等待某种资源或信号量时。当进程试图访问某个资源(如文件、网络数据或硬件设备等),但该资源当前不可用或被其他进程占用时,进程就会进入阻塞状态。在阻塞状态下,进程会释放CPU,使得其他进程可以得到执行的机会。一旦所需的资源变得可用,进程就会被唤醒并继续执行。阻塞是一种被动行为,进程在等待资源时无法主动控制自己的状态。
相比之下,挂起(Suspension)是一种更为主动的进程管理策略。挂起操作通常是由系统或用户主动发起的,用于暂停某个进程的执行。挂起的进程会被移出内存并保存到磁盘上,从而释放系统资源以供其他进程使用。挂起通常发生在需要对进程进行调试、优先级调整或资源优化等场景下。挂起状态可以通过相应的系统调用来恢复,使进程重新进入就绪状态并继续执行。
在实现方式上,阻塞是通过内核调度器自动实现的,而挂起则需要系统或用户的显式操作。此外,阻塞只涉及CPU资源的释放,而挂起则涉及进程的内存映像被保存到磁盘以及从磁盘恢复的过程。
从恢复机制来看,阻塞的进程在资源可用时会自动被唤醒,而挂起的进程则需要系统或用户通过显式操作来恢复其执行。因此,阻塞的恢复是自动的,而挂起的恢复是手动的。
总的来说,阻塞和挂起都是Linux系统中用于控制进程执行状态的重要机制。它们的主要区别在于发生时机、实现方式、资源占用以及恢复机制等方面。了解这些差异有助于我们更好地理解和应用这些机制来优化系统性能和进程管理。
2.6阻塞,挂起,和进程切换的关系
阻塞、挂起和进程切换在操作系统中都是紧密相关的概念,它们共同构成了进程管理的核心机制。
首先,阻塞和挂起都会导致进程暂停执行。阻塞通常发生在进程等待某种资源或信号量时,如等待IO操作完成或获取某个锁。而挂起则是由于系统或用户的需要,如为了节省内存或调试目的,将进程的状态保存在磁盘上并暂时停止执行。无论是阻塞还是挂起,进程都会释放CPU,使得其他进程可以得到执行的机会。
进程切换则是操作系统中用于在不同进程之间分配CPU时间片的一种机制。当一个进程执行完毕或因为某种原因(如阻塞、挂起)而暂停时,操作系统会保存该进程的上下文(包括CPU寄存器的状态、程序计数器等),并加载下一个要执行的进程的上下文,从而实现进程之间的切换。
因此,阻塞和挂起与进程切换之间存在密切的关系。当进程因为阻塞或挂起而暂停执行时,操作系统会利用这个机会进行进程切换,将CPU时间片分配给其他就绪状态的进程。这样,系统可以充分利用CPU资源,提高系统的并发性和吞吐量。
需要注意的是,进程切换本身也会带来一定的开销,如保存和加载上下文所需的时间和空间成本。因此,在设计操作系统和进程管理策略时,需要权衡阻塞、挂起和进程切换之间的关系,以优化系统性能和资源利用率。
综上所述,阻塞、挂起和进程切换在操作系统中相互关联,共同构成了进程管理的复杂机制。理解这些概念及其之间的关系对于优化系统性能和进程管理至关重要。
3.由浅入深理解线程同步
3.1竞态条件与同步机制
竞态条件
竞态条件(Race Condition)是指多个线程或进程同时访问共享资源,并且对资源的访问顺序不确定,导致最终结果的正确性依赖于线程执行的具体时序。竞态条件可能导致不可预测的结果,破坏程序的正确性和一致性。
竞态条件通常发生在多个线程或进程同时对共享资源进行读写操作时,其中至少一个是写操作。当多个线程或进程同时读写共享资源时,由于执行顺序的不确定性,可能会导致数据的不一致性、丢失、覆盖等问题。
为了避免竞态条件的发生,可以使用同步机制来限制对共享资源的访问,确保每次只有一个线程可以访问该资源。常见的同步机制包括互斥锁、信号量、条件变量等。此外,还可以使用原子操作和线程安全的数据结构来避免竞态条件的发生。
同步和互斥是并发编程中两个相关但不同的概念。
互斥是为了解决安全问题,用于保护共享资源,确保在任意时刻只有一个线程可以访问该资源。互斥机制通过引入互斥锁(Mutex)来实现,当一个线程获得互斥锁时,其他线程必须等待,直到该线程释放锁才能继续访问共享资源。
同步是在互斥保证了安全的前提下协调多个线程或进程的执行顺序,以确保它按照一定的顺序和规则高效地访问资源。同步机制可以通过互斥、信号量、条件变量等方式来实现,以确保线程或进程之间的有序执行。
同步还可以包括其他机制,如条件变量、信号量等,用于实现更复杂的同步需求,例如线程间的通信和协作。
3.2条件变量的概念
只通过互斥保护共享资源不能够完全解决竞态条件问题
饥饿问题:在资源竞争的过程中,某执行流频繁的申请到资源,而导致其他执行流被长时间地阻塞或无法获得所需的资源时,就会发生饥饿问题。【有人“撑死”,有人“饿死”】“撑死”的线程一直在对获取到的资源进行操作,我们创建多线程的目的被打破,即我们创建多线程就是让多个线程去执行任务,而现在由于某一个线程优先级或者“能力”较强,任务都让他给做完了。这不是我们想看到的。
忙等待问题:当访问临界资源时,除了要申请互斥锁,还要检测资源是否就绪。如上一章互斥锁中多线程售票的例子,还需要检测票数是否大于0,才能进行售票。当资源不就绪时,各线程只能通过频繁的轮询申请释放锁并进行检测才能确定资源是否就绪,浪费了CPU和锁资源。这就是忙等待问题。【一个线程准备访问临界资源,在访问之前得确保资源处于有绪状态/可以被访问处理的状态,如果此时资源不有绪,该线程就会释放锁,当下一次这个线程又准备访问临界资源,又得重新查看资源就绪的状态,如果还未就绪那么只能不断的申请锁,释放锁…就好比在20世纪90年代,人们通信技术不好,小明想去买手机,第一次去,货卖完了,第二次去还没有…小明只能不断地从家不远万里到超市查看手机的就绪状态,这样的情况,不仅小明花费了时间精力并且什么都没做成,老板也花费了时间精力并没有盈利(他不断地向小明解释货还没到的原因)如果有这么一种情况,即小明在家就能知道手机的就绪状态,当老板有货再通知小明来获取手机】
因此,完全解决竞态条件问题不仅仅需要通过互斥保护共享资源,还必须通过其他同步机制进行线程间的通信和协作。
条件变量
条件变量(Condition Variable)是一种线程同步机制,用于线程间的等待和唤醒操作,以实现线程间的协调和通信。条件变量通常与互斥锁(Mutex)结合使用,用于解决线程同步和竞态条件的问题。
条件变量提供了的基本操作:
等待(Wait):一个线程调用等待操作后,会释放持有的互斥锁,并进入等待状态,直到其他线程通过唤醒操作将其唤醒。在被唤醒后,线程会重新申请互斥锁,并继续执行。
唤醒(Signal):一个线程调用唤醒操作后,会选择一个或多个等待在条件变量上的线程,并将其从等待状态唤醒。被唤醒的线程会尝试获取互斥锁,并继续执行。
条件变量的使用:
- 创建并初始化条件变量:在程序中创建并初始化一个条件变量对象。
- 创建并初始化互斥锁:在程序中创建并初始化一个互斥锁对象,用于保护共享资源的访问。
- 等待条件:在需要等待某个条件满足的线程中,首先获取互斥锁,如果条件不满足,则调用条件变量的等待操作,将线程置于等待状态,同时会释放持有的互斥锁。【即如果你获取了锁但是资源没到位,你就把锁释放了,乖乖在这等着,不要再轮询查看资源的状态,有“人”会通知你,等收到唤醒通知再醒来继续后续操作】
- 发送条件变量信号:当满足某个条件时,可以发送条件变量信号,通知等待的线程继续执行。可以使用pthread_cond_signal函数发送信号,唤醒一个线程。有时候需要同时唤醒多个等待的线程,可以使用pthread_cond_broadcast函数进行广播。
- 接收条件变量信号:在因资源未就绪而处于等待的线程中,收到条件变量信号后,线程被唤醒,再次尝试申请锁,如果申请锁成功则继续向后执行。
条件变量需要与互斥锁配合使用,以保证对共享资源的访问是互斥的。对条件变量的等待和唤醒操作需要在持有互斥锁的情况下进行,以确保操作的原子性和正确性。
条件变量的等待是一种排队阻塞等待的操作,线程会按照先进先出的顺序等待条件变量的唤醒。这在一定程度上解决了多线程的饥饿问题。
通过条件变量,线程可以在满足特定条件之前阻塞等待,而不是忙等待,从而提高了系统的效率和性能。同时在阻塞等待之前,它会释放所持有的互斥锁,允许其他线程获得该互斥锁并继续执行。这样,其他线程就有机会在等待期间获取互斥锁,并访问共享资源。这也在一定程度上解决了多线程的饥饿问题。
条件变量县
使用接口 代码测试
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#define THREAD_NUM 4
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
typedef void (*func_t)(const std::string &threadName, pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;
class ThreadData
{
public:
ThreadData(const std::string &threadName, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
: _name(threadName),
_func(func),
_pmtx(pmtx),
_pcond(pcond)
{
}
public:
std::string _name;
func_t _func;
pthread_mutex_t *_pmtx;
pthread_cond_t *_pcond;
};
void login(const std::string &threadName, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while (!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx); // 线程刚开始执行 就调用wait 当前线程会被立即被阻塞
std::cout << threadName << " running... -- login::登录" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void load(const std::string &threadName, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while (!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);
std::cout << threadName << " running... -- load::加载" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void download(const std::string &threadName, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while (!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);
std::cout << threadName << " running... -- download::下载" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
void send(const std::string &threadName, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while (!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);
std::cout << threadName << " running... -- send::发送" << std::endl;
pthread_mutex_unlock(pmtx);
}
}
// 启动例程 --- 新线程入口函数
void *Entry(void *args)
{
ThreadData *td = (ThreadData *)args; // td在每一个线程自己私有的栈空间中保存
td->_func(td->_name, td->_pmtx, td->_pcond); // td->_func是一个函数 调用完成后就会返回
delete td;
return nullptr;
}
int main()
{
pthread_t tids[THREAD_NUM];
func_t funcs[THREAD_NUM] = {login, load, download, send};
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);
pthread_cond_t cond;
pthread_cond_init(&cond, nullptr);
// 创建THREAD_NUM个线程
for (int i = 0; i < THREAD_NUM; i++)
{
std::string threadName = "Thread " + std::to_string(i + 1);
ThreadData *td = new ThreadData(threadName, funcs[i], &mtx, &cond);
pthread_create(tids + i, nullptr, Entry, (void *)td);
}
// 观察:主线程创建了新线程 新线程都在wait
// 主线程也不去唤醒 此时应该是一个没有线程在做事的空窗期
sleep(2);
// 唤醒等待在指定条件变量上的线程
int count = 10;
while (count != 0)
{
std::cout << "唤醒线程...唤醒倒计时:" << count-- << std::endl;
// pthread_cond_broadcast(&cond);唤醒等待在指定条件变量上的所有线程
pthread_cond_signal(&cond); // 唤醒等待在指定条件变量上的单个线程
sleep(1);
}
std::cout << "停止唤醒!" << std::endl;
quit = true;
pthread_cond_broadcast(&cond);
// 主线程阻塞等待其余线程
for (int i = 0; i < THREAD_NUM; i++)
{
pthread_join(tids[i], nullptr);
std::cout << "thread" << i + 1 << ": " << tids[i] << " quit " << std::endl;
}
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
当一个线程调用pthread_cond_wait时,它会释放所持有的互斥锁进入等待状态。该线程会被放入条件变量的等待队列中,按照先进先出的顺序排队等待。因此线程的执行具有一定的顺序。
我们无法确定到底哪一个线程先运行,因为线程的首次运行顺序(等待队列的顺序)完全由操作系统调度器决定。
总结线程同步/条件变量
- 一个线程需要访问临界资源,如果我们不对其进行条件变量的控制,那么他就可能出现不断地访问该资源,使得其他线程无法访问且在不断地轮询检测资源的状态,这种情况不错但是不合理。引入同步的机制,主要是为了解决 访问临界资源的合理性问题即让线程按照一定的顺序,进行临界资源的访问。
- 当一个线程想要访问临界资源前,先要检测临界资源是否处于有绪状态即是否可以被访问,而“检测”的这个行为本质也是在访问临界资源,那么对临界资源的检测也一定是需要在加锁和解锁之间。
- 常规方式要检测条件就绪,注定了线程必须频繁申请和释放锁
- 有没有办法让线程检测到资源不就绪的时候,不要让线程在频繁自己检测,而是让他等待,当条件就绪的时候,通知对应的线程,让他来进行资源申请和访问。 ⇒ 条件变量!
上述提到,为了提高多线程的效率以及彻底达到多线程的目的即让多线程去高效的做任务,我们想让条件满足的时候再唤醒指定的线程 — 怎么知道条件是否满足?
控制/生成这个资源的一方知道什么时候达到了“满足”状态。比如,生产者生产了资源,那么生产者就知道仓库里有资源,那么他就可以通知消费者来获取。又比如,消费者获取了资源,那么消费者就知道仓库里有地方可以存放资源,那么他就可以通知生产者来生产。