2-6-1-1 QNX编程入门之进程和线程(八)
阅读前言
本文以QNX系统官方的文档英文原版资料“Getting Started with QNX Neutrino: A Guide for Realtime Programmers”为参考,翻译和逐句校对后,对在QNX操作系统下进行应用程序开发及进行资源管理器编写开发等方面,进行了深度整理,旨在帮助想要了解QNX的读者及开发者可以快速阅读,而不必查看晦涩难懂的英文原文,这些文章将会作为一个或多个系列进行发布,从遵从原文的翻译,到针对某些重要概念的穿插引入,以及再到各个重要专题的梳理,大致分为这三个层次部分,分不同的文章进行发布,依据这样的原则进行组织,读者可以更好的查找和理解。
1. 进程和线程
1.4. 关于同步的更多信息
2-6-1-1 QNX编程入门之进程和线程(一)
2-6-1-1 QNX编程入门之进程和线程(二)
2-6-1-1 QNX编程入门之进程和线程(三)
2-6-1-1 QNX编程入门之进程和线程(四)
2-6-1-1 QNX编程入门之进程和线程(五)
2-6-1-1 QNX编程入门之进程和线程(六)
2-6-1-1 QNX编程入门之进程和线程(七)
接前面章节内容继续。
1.4.3. 条件变量
条件变量(或 condvars)与我们刚才看到的 sleepon 锁非常相似。 事实上, sleepon 锁就是建立在条件变量之上的,这就是为什么我们在 的解释表中为 sleepon 示例提供了 CONDVAR 状态。需要重复的是,pthread_cond_wait() 函数释放互斥,等待,然后重新获取互斥,就像 pthread_sleepon_wait() 函数所做的一样。
让我们跳过前导部分,用 condvars 代替睡眠部分中生产者和消费者的例子。然后我们来讨论调用。
/*
* cp1.c
*/
#include <stdio.h>
#include <pthread.h>
int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;
void *consumer (void *notused)
{printf ("In consumer thread...\n");while (1) {pthread_mutex_lock (&mutex);while (!data_ready) {pthread_cond_wait (&condvar, &mutex);}// process dataprintf ("consumer: got data from producer\n");data_ready = 0;pthread_cond_signal (&condvar);pthread_mutex_unlock (&mutex);}
}
void *producer (void *notused)
{printf ("In producer thread...\n");while (1) {// get data from hardware// we'll simulate this with a sleep (1)sleep (1);printf ("producer: got data from h/w\n");pthread_mutex_lock (&mutex);while (data_ready) {pthread_cond_wait (&condvar, &mutex);}data_ready = 1;pthread_cond_signal (&condvar);pthread_mutex_unlock (&mutex);}
}
main ()
{printf ("Starting consumer/producer example...\n");// create the producer and consumer threadspthread_create (NULL, NULL, producer, NULL);pthread_create (NULL, NULL, consumer, NULL);// let the threads run for a bitsleep (20);
}
与我们刚才看到的 sleepon 示例基本相同,只是有一些变化(我们还添加了一些 printf() 函数以及一个 main() 函数,这样程序就能运行了!)。我们首先看到的是一个新的数据类型:pthread_cond_t。这只是条件变量的声明;我们称之为 condvar。
接下来我们注意到,消费者的结构与之前的 sleepon 示例中的消费者完全相同。我们将 pthread_sleepon_lock()
和 pthread_sleepon_unlock()
替换为标准的互斥版本(pthread_mutex_lock()
和 pthread_mutex_unlock()
)。pthread_sleepon_wait()
被 pthread_cond_wait()
取代。主要区别在于, sleepon 库中深藏着一个互斥,而当我们使用 condvars 时,我们需要显示调用互斥锁。 这样我们就能获得更大的灵活性。
最后,我们注意到我们使用了 pthread_cond_signal()
, 而不是 pthread_sleepon_signal()
。
1.4.3.1. 发信号(signal)与广播(broadcast)
在 sleepon 部分,我们承诺会讨论 pthread_sleepon_signal()
和 pthread_sleepon_broadcast()
函数之间的区别。同样,我们还将讨论两个 condvar
函数 pthread_cond_signal()
和 pthread_cond_broadcast()
的区别。
简而言之: "signal" 版本的函数只能唤醒一个线程。因此,如果有多个线程被阻塞在 "wait" 函数中,而某个线程执行了 "signal" 操作,那么只有一个线程会被唤醒。哪一个?优先级最高的那个。如果有两个或更多的线程具有相同的优先级, 那么唤醒的顺序就不确定了。在 "broadcast" 版本中, 所有 被阻塞的线程都会被唤醒。
唤醒所有线程似乎有些浪费。另一方面,如果只唤醒一个线程(实际上是随机的),又会显得草率。
因此,我们应该看看在哪些情况下使用一种方法比使用另一种方法更有意义。很明显,如果只有一个线程在等待,就像我们在消费者程序的任何一个版本中所做的那样,使用 "signal"版本的函数就可以了。
在多线程情况下,我们不得不问:“为什么这些线程在等待?”这通常有两种可能的答案:
• 所有线程都被认为是等价的,并且有效地形成了一个可用线程 "池(pool)",随时准备处理某种形式的请求。
• 这些线程都是独一无二的,每个线程都在等待一个非常特殊的条件出现。
在第一种情况下,我们可以想象所有线程的代码可能如下所示:
/*
* cv1.c
*/
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex_data = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv_data = PTHREAD_COND_INITIALIZER;
int data;
thread1 ()
{for (;;) {pthread_mutex_lock (&mutex_data);while (data == 0) {pthread_cond_wait (&cv_data, &mutex_data);}// do somethingpthread_mutex_unlock (&mutex_data);}
}
// thread2, thread3, etc have the identical code.
在这种情况下, 哪个线程获得数据并不重要,只要其中一个线程获得数据并对其进行处理即可。
但是,如果你有这样的东西,情况就有点不同了:
/*
* cv2.c
*/
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex_xy = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv_xy = PTHREAD_COND_INITIALIZER;
int x, y;
int isprime (int);
thread1 ()
{for (;;) {pthread_mutex_lock (&mutex_xy);while ((x > 7) && (y != 15)) {pthread_cond_wait (&cv_xy, &mutex_xy);}// do somethingpthread_mutex_unlock (&mutex_xy);}
}
thread2 ()
{for (;;) {pthread_mutex_lock (&mutex_xy);while (!isprime (x)) {pthread_cond_wait (&cv_xy, &mutex_xy);}// do somethingpthread_mutex_unlock (&mutex_xy);}
}
thread3 ()
{for (;;) {pthread_mutex_lock (&mutex_xy);while (x != y) {pthread_cond_wait (&cv_xy, &mutex_xy);}// do somethingpthread_mutex_unlock (&mutex_xy);}
}
在这种情况下,唤醒一个线程是不够的!我们必须唤醒所有三个线程,并让每个线程检查变量是否满足条件。
这很好地反映了上述问题的第二种情况("为什么这些线程在等待?)由于这些线程都在等待不同的条件( thread1()
等待 x 小于或等于 7 或 y 为 15,thread2()
等待 x 为质数,thread3()
等待 x 等于 y),我们别无选择,只能唤醒所有线程。
1.4.3.2. 睡眠锁(sleepons)与条件变量(condvars)的比较
与 condvars
相比,sleepons
有一个主要优势。假设你想同步许多对象。在使用 condvar
时,通常是每个对象关联一个 condvar
。因此,如果您有 M 个 对象,您很可能会有 M 个 condvars
。而使用 sleepons
时, 底层的condvars
(sleepons
就是在其基础上实现的)是在线程等待特定对象时动态分配的。因此,在有 M 个对象和 N个线程阻塞的情况下使用 sleepons
,你最多会有 N 个 condvars
(而不是 M 个)。
不过, condvars
比 sleepons
更灵活,因为
- 反正
sleepons
是建立在condvars
的基础上的。 sleepons
会在库中埋入互斥锁;而condvars
则需要你显示调用互斥锁。
第一点可能会被认为富有争议。 然而,第二点的意义更重大。如果将互斥锁埋藏在库中,就意味着每个进程只能有一个互斥锁,无论该进程中有多少个线程,也无论有多少个不同的数据变量"集"。这可能是一个非常有限制性的因素,尤其是当你考虑到必须使用唯一的一个互斥来访问进程中任何线程需要接触的所有数据变量时!
更好的设计是使用多个互斥锁,每个数据集一个,并根据需要明确地将它们与条件变量结合起来。这种方法的真正威力和危险之处在于,完全没有编译或运行时检查来确保您的系统是安全的:
- 在操作变量之前锁定了互斥
- 正在为特定变量使用正确的互斥
- 使用的是正确的
condvar
条件变量,带有适当的互斥锁和变量
要解决这些问题,最简单的方法就是进行良好的设计和设计审查,同时借鉴面向对象编程的技术(如将互斥锁包含在数据结构中, 有访问数据结构的例程等)。当然,两者的应用程度不仅取决于个人风格,还取决于性能要求。使用 condvars
时要记住以下要点:
- 互斥将用于测试和访问变量。
- 这个 condvar 将被用作会合点。
这是一张图片:

一个有趣的说明。因为没有检查,所以你可以这样做,比如将一组变量与互斥锁"ABC"关联,将另一组变量与互斥锁"DEF"关联,同时将两组变量都与条件变量"ABCDEF"进行关联。

这实际上非常有用。由于互斥锁总是用于 "访问和测试",这意味着每当我想查看某个变量时,就必须选择正确的互斥锁。这很公平,如果我要查看变量 "C",我显然需要锁定互斥锁 "MutexABC
"。如果我改变了变量 "E "呢?那么,在修改之前,我必须先获取互斥锁 "MutexDEF
"。然后,我修改了它,并 hit
条件变量 "CondvarABCDEF
",告诉其他人我修改了它。此后不久,我将释放互斥锁。
现在来看看会发生什么。突然间,我有一堆一直在等待 "CondvarABCDEF
"的线程现在被唤醒了(从它们的pthread_cond_wait()
中唤醒)。等待函数立即尝试重新获取互斥锁。这里的关键是有两个互斥锁需要获取。这意味着在 SMP 系统上,可以运行两个并发的线程流,每个线程使用独立的互斥锁检查它认为独立的变量。很酷吧?
未完待续,请继续关注本专栏内容……