在讲互斥量之前,先明确下面两个概念:
竞争条件:两个或多个进程(或线程)读写某些共享数据,最终结果取决于进程(或线程)运行的精确时序,被称为竞争条件。
临界区: 对共享内存进行访问的程序片段。
通过适当安排使得两个进程(或线程)不可能同时处于临界区,就能够避免竞争条件。
那么如何才能避免竞争条件呢?如何才能使两个线程不能同时访问共享资源呢?关键是要互斥,即当一个线程在使用一个共享资源时,其他线程不能做同样的操作。
为避免竞争条件,一个好的解决方案应当满足以下条件:
- 任何两个进程(线程)不能同时处于其临界区
- 不应对CPU的速度和数量做任何假设
- 临界区外运行的进程(线程)不能阻塞其他进程(线程)
- 不能使进程(线程)无限期等待进入临界区
锁变量
我们可以设置一个锁变量,其初始值为0,。当一个线程要进入临界区时, 先测试这个变量,如果它的值为0,就将其置为1并进入临界区,从临界区退出时再将其恢复为0. 如果进入临界区前锁变量的值已经为1,则这个线程将等待直到它的值变为0.
不过这样的锁变量是存在疏漏的:假设一个线程读出锁变量的值为0,而在它将锁变量置为1之前,另一个线程被调度运行,此时第二个线程读出锁变量的值依然为0, 所以它可以将锁置为1,并进入临界区。之后第一个线程再次能运行时,它同样将锁变量置为1,并进入临界区。此时就有两个线程处于临界区了,显然竞争条件产生了。
严格轮换
看下面一段代码
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int n = 0;
int flag = 1;
void *pth_func1(void *args)
{
while(1)
while(flag == 1) {
for (int i = 0; i < 5; i++)
printf("in thread %ld : %d\n", (unsigned long) args, n++);
flag = 2;
printf("in thread %ld : flag turn to %d\n", (unsigned long) args, flag);
}
}
void *pth_func2(void *args)
{
while(1)
while(flag == 2) {
for (int i = 0; i < 5; i++)
printf("in thread %ld : %d\n", (unsigned long) args, n++);
flag = 1;
printf("in thread %ld : flag turn to %d\n", (unsigned long) args, flag);
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, pth_func1, (void*)1);
pthread_create(&tid2, NULL, pth_func2, (void*)2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
}
线程1和线程2拥有一个共享变量n,每当它们要访问n之前都会先测试变量flag,只有当flag等于它们的线程编号时才会进入临界区,从临界区退回时会将flag置为另一个线程的编号。如果测试flag不等于自己的线程编号,那么线程会在一个循环中不断地测试flag的值,直到相等。
这种连续测试的方式被称为忙等待。通过上面的代码我们也可以看出这两个线程会严格地交替运行,所以这种方式就叫严格轮换,可以看出这种方式弥补了上述锁变量的缺陷,但是如果两个线程的循环时间相差太多的话,势必会有一个线程耗费太多的时间在忙等待的过程中,这是对CPU资源的浪费。
下面是上述程序的部分输出
in thread 1 : 0
in thread 1 : 1
in thread 1 : 2
in thread 1 : 3
in thread 1 : 4
in thread 1 : flag turn to 2
in thread 2 : 5
in thread 2 : 6
in thread 2 : 7
in thread 2 : 8
in thread 2 : 9
in thread 2 : flag turn to 1
in thread 1 : 10
in thread 1 : 11
in thread 1 : 12
in thread 1 : 13
in thread 1 : 14
in thread 1 : flag turn to 2
...
互斥量
互斥量是一个可以处于两态之一的变量:解锁和加锁。互斥量的使用类似于前面提到的锁变量,不过与用户自己定义的锁变量不同,互斥量的数据类型和相关的接口是由系统提供的,在系统的实现中就规避了上面提到的两个线程同时进入临界区的情况。
pthread提供了一个互斥量类型pthread_mutex_t, 一个线程如果想要进入临界区,它首先尝试锁住相关的互斥量,如果互斥量没有加锁,那么这个线程可以立即进入,并且该线程自动锁住相关的互斥量以防止其他线程进入。如果互斥量已经被加锁,则调用线程阻塞,直到该互斥量被解锁。如果多个线程在等待同一个互斥量,则解锁后只有一个线程会被允许运行。
互斥量相比严格轮换有如下优点:
- 在使用互斥量的线程中,如果取锁失败,则线程会调用thread_yield将CPU放弃给另一个线程,在下一次运行时在尝试取锁。这样相对于忙等待,提高了CPU的使用效率。并且在一个用户级线程中,处于忙等待的线程会一直循环下去,而不会将CPU让给其他的线程使用。好在pthread线程是混合实现的线程,所以在上面的那个程序中没有发生这样的情况。
- 由于严格轮换的机制,线程必须轮流进入临界区,而互斥量没有这样的要求,一个线程可以连续多次进入临界区,这样显然更加灵活。
pthread提供了关于互斥量的以下接口
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//用于初始化一个互斥量,attr为属性选项,默认为NULL
//注意在使用互斥量之前必须先对其初始化,也可以把他设置为常量PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//用于撤销一个互斥量,如果动态分配了一个互斥量,在释放内存前要先调用该函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
//对互斥量加锁,如果已上锁,则阻塞调用线程直到互斥量被解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//尝试对互斥量加锁,如果成功则直接返回0,若失败也不会阻塞进程,而是返回EBUSY
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//对互斥量解锁
下面的程序是将上述严格轮换部分的示例改写为使用互斥量后的代码,注意这里每个线程只会循环三次
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
int n = 0;
int flag = 1;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER ;
void *pth_lock(void *args)
{
for(int i = 0; i < 3; i++){
pthread_mutex_lock(&mutex1);
for(int i = 0; i < 5; i++)
printf("in thread %ld : %d\n", (unsigned long)args, ++n);
pthread_mutex_unlock(&mutex1);
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, pth_lock, (void*)1);
pthread_create(&tid2, NULL, pth_lock, (void*)2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
}
输出结果
由结果我们也可以看出,使用互斥量的线程并不是严格轮换的
in thread 1 : 1
in thread 1 : 2
in thread 1 : 3
in thread 1 : 4
in thread 1 : 5
in thread 1 : 6
in thread 1 : 7
in thread 1 : 8
in thread 1 : 9
in thread 1 : 10
in thread 1 : 11
in thread 1 : 12
in thread 1 : 13
in thread 1 : 14
in thread 1 : 15
in thread 2 : 16
in thread 2 : 17
in thread 2 : 18
in thread 2 : 19
in thread 2 : 20
in thread 2 : 21
in thread 2 : 22
in thread 2 : 23
in thread 2 : 24
in thread 2 : 25
in thread 2 : 26
in thread 2 : 27
in thread 2 : 28
in thread 2 : 29
in thread 2 : 30
本文介绍了在多线程编程中如何使用互斥量来避免竞争条件,对比了互斥量与严格轮换的区别,并提供了具体的代码示例。
1694

被折叠的 条评论
为什么被折叠?



