本文描述如何使用 C 编程语言和标准 POSIX 线程。
使用互斥锁和信号量管理并发访问
如前所述,并发写入操作可能会导致大量问题。由于线程经常操作相同的共享数据,因此开发人员必须密切关注在任何给定时间允许访问变量或文件的线程数量。使用信号量的互斥锁(mutex)是防止太多线程同时访问一个文件或变量的一种方法。
在此示例中,每个线程必须完全完成将值写入关键部分中的变量,然后其他线程才能进入该部分。无论线程何时进入或退出该节,它都会输出其 ID 和一个短状态(正在运行或已结束)。在左图中,您可以看到两个线程同时运行。此外,操作系统还允许许多其他线程在线程0完成之前运行。在正确的映像中,所有线程都按预期启动和停止。
信号量是一个容易理解的简单构造,它们通常不属于特定的编程语言。相反,高级操作系统提供了这种机制。可以将信号量视为具有队列的计数器。在程序中,定义要允许多少个线程同时输入关键部分,例如代码中的文件访问操作。然后,每当一个线程想要进入这个部分时,它就会向信号量机制请求权限。如果计数大于零,信号量允许请求线程进入该区域。否则,它将线程放在队列的末尾。然后操作系统挂起线程,直到轮到线程进入关键部分。一旦线程进入该部分并随后再次退出,它就向信号量发出信号,表示另一个线程现在可以执行关键代码。
考虑下面的例子:
- #define NUM_THREADS 8#define LOOP_ITERAtiONS 1000pthread_t threads[NUM_THREADS];int result = 0;void* createThread(void* id){ int tmp; for(int i = 0; i < LOOP_ITERATIONS; i++) { tmp = result; tmp = tmp + 1; result = tmp; } return NULL;}int main(int argc, char* argv[]){ for(int i = 0; i < NUM_THREADS; i++) { pthread_create(&threads, NULL, createThread, NULL); } for(int i = 0; i < NUM_THREADS; i++) pthread_join(threads, NULL); printf("The result is: %d
- ", result); return 0;}
复制代码
这个看起来简单、没有问题的多线程程序看起来不怎么样,但是它足以导致严重的并发访问问题。首先,注意全局 result 变量,并回想一下这个程序中的所有线程共享同一个变量。但是,每个线程都拥有位于 createThread 函数中的本地 tmp 变量的副本。然后,每个线程获取全局变量的一个副本,修改它,然后将结果写回全局变量。但是,当其他线程继续更新全局变量时,线程可能在任何时候暂停代码执行。因此,许多更新会丢失,并且根据执行顺序,结果会有所不同:
结果应该始终是8000。然而,根据线程的执行顺序,结果会有很大的不同。
使用信号量,程序员可以保护关键区域,以便在任何给定时间只有一个线程可以访问全局变量:
Copy Code
/* Include statements omitted */#define NUM_THREADS 8#define LOOP_ITERATIONS 1000sem_t semaphore;pthread_t threads[NUM_THREADS];int result = 0;void* createThread(void* id){ int tmp; for(int i = 0; i < LOOP_ITERATIONS; i++) { sem_wait(semaphore); tmp = result; tmp = tmp + 1; result = tmp; sem_post(semaphore); } return NULL;}int main(int argc, char* argv[]){ sem_init(&semaphore, 0, 1); for(int i = 0; i < NUM_THREADS; i++) pthread_create(&threads, NULL, createThread, NULL); for(int i = 0; i < NUM_THREADS; i++) pthread_join(threads, NULL); sem_close(&semaphore); return 0;}
此代码示例的新版本包含一个全局信号量变量,main 方法的第一行初始化该信号量。初始化调用的最后一个参数定义有多少个线程可以同时进入关键区域。然后,程序像之前一样生成多个线程。但是,在 createThread 方法的循环中,每个线程在使用变量之前检查它是否可以访问该变量。接下来,sem _ wait 方法请求访问,sem _ post 方法向变量发出信号,表示线程已离开关键部分。最后,main 方法的倒数第二行关闭信号量变量。请注意,信号量是特定于操作系统的,因此方法的确切名称可能会有所不同。但是,一般原则适用于所有操作系统和实现。
使用锁实现互斥锁如果您认为信号量对于实现简单的互斥锁来说太强大了,那么您还可以使用一个不需要信号量的特殊锁函数。这里,一个线程在进入临界区域之前调用 lock 函数,然后通过调用解锁函数释放锁:
Copy Code
#define NUM_THREADS 8#define LOOP_ITERATIONS 1000pthread_mutex_t lock;pthread_t threads[NUM_THREADS];int result = 0;void* createThread(void* id){ int tmp; for(int i = 0; i < LOOP_ITERATIONS; i++) { pthread_mutex_lock(&lock); tmp = result; tmp = tmp + 1; result = tmp; pthread_mutex_unlock(&lock); } return NULL;}int main(int argc, char* argv[]){ for(int i = 0; i < NUM_THREADS; i++) pthread_create(&threads, NULL, createThread, NULL); for(int i = 0; i < NUM_THREADS; i++) pthread_join(threads, NULL); return 0;}
您可以看到锁调用发生在与前面的二进制信号量调用相同的位置。您可能想知道为什么要在锁上使用信号量。无需详细说明,锁始终与一个线程相关联,只有该线程可以解锁该区域。因此,如果该线程崩溃、冻结或进入死锁状态,无论是操作系统还是更高优先级的线程都无法在不结束整个程序的情况下解锁关键部分。除了其他原因,信号量也更加灵活,因为它们可以允许多个线程进入一个区域,而不仅仅是一个线程。
如何在多线程 C 程序中实现条件同步
当您回想上一篇文章中讨论的另一个大问题时,信号量增加的灵活性就变得很明显了。除了管理关键区域之外,并发线程有时还必须以特定的顺序运行,这可以通过使用信号量来实现。例如,假设线程 A 必须总是在线程 B 执行任务 Y 之前完成任务 X。您可以通过使用信号量来实现这个时间表:
Copy Code
/* import statements omitted */#define LOOP_ITERATIONS 10000#define MULTIPLIER 100int result = 0;sem_t semaphore;void* createThreadB(void* id){ sem_wait(semaphore); result = result * MULTIPLIER; return NULL;}void* createThreadA(void* id){ for(int i = 0; i < LOOP_ITERATIONS; i++) result = result + 1; sem_post(semaphore); return NULL;}int main(int argc, char* argv[]){ pthread_t threadA; pthread_t threadB; sem_init(&semaphore, 0, 0); pthread_create(&threadA, NULL, createThreadA, NULL); pthread_create(&threadB, NULL, createThreadB, NULL); pthread_join(threadA, NULL); pthread_join(threadB, NULL); sem_close(&semaphore); // Should be 1.000.000 printf("The result is: %d
", result); return 0;}
请注意,在本例中,main 方法是如何创建起始值为零的信号量的。线程 A 不等信号量,而线程 B 等信号量。因此,线程 A 始终可以在 createThreadA 方法内运行代码,而不必等待任何其他线程。一旦线程 A 完成其代码的执行,它就增加信号量。这样做使线程 B 也能够运行。但是,请注意,线程 A 总是在线程 B 开始执行任何代码之前完成其任务。这个简单的示例说明了如何使用信号量实现条件同步。
摘要
本文讨论了在多线程 C 程序中使用 POSIX 线程实现互斥锁和条件同步的几种方法。如前所述,信号量是高级操作系统实现的一个引人注目的概念。实际上,信号量包含一个计数器变量和一个队列。每当一个线程想要进入一个关键区域时,它就会请求信号量的权限。如果信号量计数器大于零,则线程可以进入。否则,操作系统将该线程放在等待列表中,并允许它在另一个线程退出关键区域时继续运行。二进制信号量可以对互斥锁进行建模,而且使用简单的锁也可以得到同样的结果。
但是,信号量比简单的锁更灵活,您可以使用它们来模拟各种其他情况。例如,信号量也可以实现条件同步。在这里,将信号量的计数器初始化为值为零。然后,其中一个线程可以立即继续,而另一个请求可以从信号量访问关键区域。当然,信号量会阻止第二个线程的访问。一旦第一个线程完成了它的任务,它就会增加信号量计数器,第二个线程就可以运行了。此过程保证第一个线程始终在第二个线程之前运行。
|