线程同步:互斥量与读写锁的异同与性能分析

作者:ifndef 和c/c++相关  

线程同步简单的说就是当多个线程共享相同的内存时,当某个线程可以修改变量,而其他线程也可以读取或修改这个变量的时候,就需要对这些线程进行同步,以确保他们在访问变量的存储内容时不会访问到无效的数值。
我们来看个上面一个对于多线程访问共享变量造成竞争的一个例子

假设增量操作分为以下三个步骤
(1)从内存单元读入寄存器
(2)在寄存器中进行变量值的增加
(3)把新的值写回内存单元

那么当两个 非同步线程对同一个变量做增操作时就可能出现下面这种情况




也就是说如果两个人线程试图在几乎在同一时刻对同一个变量做增操作而不进行同步的话。结果就可能出现不一致。
变量的值可能比原来增加了一也可能比原来的值增加了2.具体是1还是2.取决于第二个线程执行第一步的时间。
如果是在第一个线程的第一步和第三步之间那么第二个线程读到的数值就和第一个线程读到的一样
(因为第一个线程还没有回写更新后的数据)。那么变量的值只增加了1.否则变量增加的是2.

那么如何解决这个问题呢,如果对共享变量的修改是原子操作那么就不存在这种竞争。但是现代的计算机
存储访问周期通常需要多个总线周期。那么怎么解决这个矛盾呢。

下面我们介绍两种线程同步的方案,并分析他们的异同和性能。

1互斥量:
 我们可以吧互斥量想成一把锁。在访问共享资源前 对互斥量进行加锁,在访问完后释放互斥量上的锁。
 对互斥量进行加锁后,任何其他试图再对该互斥量加锁的线程都会被阻塞直到当前持有锁的线程释放锁。

互斥量提供下列接口:
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex,    //初始化互斥量
   const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);  //回收非配给该互斥量的资源
      
int pthread_mutex_lock(pthread_mutex_t *mutex);    //对互斥量加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //释放加在互斥量上的锁

如果不希望线程被阻塞可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果此时互斥量未被加锁,那么pthread_mutex_trylock将锁住互斥量.否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY

我们来具体看个 两个线程竞争访问共享变量的例子


两个线程都可以访问共享变量 i ,在两个线程中都将这个变量增加一百万,那么我们期望
当两个线程都结束时,变量 i的值 应该是 两百万

int i=0;
void *thread_1(void *arg){
        int j;
        for(j=0;j<1000000;j++){
                i++;
        }  
        pthread_exit((void *)0);
}

void *thread_2(void *arg){
        int j;
        for(j=0;j<1000000;j++){
                i++;
        }  
        pthread_exit((void *)0);
}
int main(void){
        int err;
        pthread_t th1,th2;
        err=pthread_create(&th1,NULL,thread_1,(void *)0);
        if(err!=0){
                printf("create new thread error:%s\n",strerror(err));
                exit(0);
        }  
        err=pthread_create(&th2,NULL,thread_2,(void *)0);
        if(err!=0){
                printf("create new thread error:%s\n",strerror(err));
                exit(0);
        }  
           
        err=pthread_join(th1,NULL);
        if(err!=0){
                printf("wait thread done error:%s\n",strerror(err));
                exit(1);
        }
        err=pthread_join(th2,NULL);
        if(err!=0){
                printf("wait thread done error:%s\n",strerror(err));
                exit(1);
        }

        printf("i=%d\n",i);
exit(0);
}
我让程序运行了10次 输出如下:
i=2000000
i=2000000
i=2000000
i=1000000
i=2000000
i=1000000
i=1587537
i=1000000
i=2000000

从输出我们能看出,程序并没用像我们期望的那样每次的结果都是 两百万 。
这就说明 两个线程对变量 i的增 操作产生了竞争

我们来用上面提供的互斥量接口 再每次对 i 进行增加操作时都先锁住互斥量,在增加完后再释放互斥量
int i=0;
/* + */pthread_mutex_t mutex;

void *thread_1(void *arg){
        int j;
        for(j=0;j<1000000;j++){
 /* + */        pthread_mutex_lock(&mutex);
                i++;
 /* + */        pthread_mutex_unlock(&mutex);
        }  
        pthread_exit((void *)0);
}

void *thread_2(void *arg){
        int j;
        for(j=0;j<1000000;j++){
 /* + */        pthread_mutex_lock(&mutex);
                i++;
  /* + */       pthread_mutex_unlock(&mutex);
        }  
        pthread_exit((void *)0);
}
int main(void){
        int err;
        pthread_t th1,th2;
/* + */ pthread_mutex_init(&mutex,NULL);
        err=pthread_create(&th1,NULL,thread_1,(void *)0);
        if(err!=0){
                printf("create new thread error:%s\n",strerror(err));
                exit(0);
        }  
        err=pthread_create(&th2,NULL,thread_2,(void *)0);
        if(err!=0){
                printf("create new thread error:%s\n",strerror(err));
                exit(0);
        }

        err=pthread_join(th1,NULL);
        if(err!=0){
                printf("wait thread done error:%s\n",strerror(err));
                exit(1);
        }
        err=pthread_join(th2,NULL);
        if(err!=0){
                printf("wait thread done error:%s\n",strerror(err));
                exit(1);
        }

        printf("i=%d\n",i);
/* + */ pthread_mutex_destroy(&mutex);
exit(0);
}

修改的地方只是  加锁解锁和初始化与释放 用 + 号标出
现在再来看看输出情况
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000

这次的输出正是我们期望的,每次运行 i 变量都增加为 两百万。并且没个i的输出都会停顿一下,因为加锁和解锁影响了程序的速度


下面介绍第二中方案
写锁:

回想一下上面介绍的互斥量,它只有两个状态,要么是加锁状态,要么是不加锁。假如现在一个线程 a 只是想读一个共享变量 i ,因为不确定是否会有线程去写他,所以我们还是要对它进行加锁。但是这时候又一个线程 b 试图读共享变量i ,于是发现被锁住,那么b不得不等到a释放了锁后才能获得锁并读取 i 的值,
但是两个读取操作即使是几乎同时发生也并不会像写操作那样造成竞争,应为他们不修改变量的值。所以我们期望如果是 多个线程试图读取共享变量的 值的话,那么他们应该可以立刻获取而不需要等待前一个线程释放因为读而加的锁。

读写锁解决了上面的问题。他提供了比互斥量跟好的并行性。因为以读模式加锁后当又有多个线程仅仅是试图再以读模式加锁然时,并不会造成这些线程阻塞在等待锁的释放上。
注意我们只是说 读写锁提高了 更高的并行性,也就是说允许多个线程几乎同一时间读取一个共享变量,而互斥锁不行。但是不是 使用读写锁就会比互斥量快呢?我们下面会做一些测试


读写锁的特点是:
当读写锁是写加锁时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁是读加锁时,在这个锁被解锁之前,所有试图以读模式对他进行加锁的线程都可以得到访问权,但是如果线程以写模式对此锁加锁时会造成阻塞,直到所有线程释放读锁

这里有一个问题,如果一个线程现 A 获得一把读锁,现在来了一个线程B请求写锁,那么它就要等到线程A释放读锁,但是如果在A释放之前又一个线程获得了读锁(读锁可以多次获得),
并且在改线程释放读锁之前又一个线程请求到了读锁,假设后续一直有读锁请求成功,那么请求写锁的进程B就不得不一直阻塞等待所有读锁的释放。所以相关的实现一般让当一个读写锁处于读模式锁住时,此时来了另一个线程的写模式加锁请求,那么读写锁通常会阻塞随后的读模式锁请求,这样可以避免锁被读模式长期占用,而写模式请求一直得不到满足。

读写锁提供了类似的接口:
#incldue
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
   const pthread_rwlockattr_t *restrict attr);  //初始化读写锁
int pthread_rwlock_deatroy(pthread_rwlock_t *rwlock); //回收非配给读写锁的资源

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读模式加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写模式加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//释放锁

下面这两个函数为尝试加锁,当可以加锁时,会获得锁,不可以时不会阻塞而是返回错误EBUSY
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlick_trywrlock(pthread_rwlock_t *rwlock);


我们直接以上面的测试互斥量的那个例子 来比较 读写锁和互斥量的性能。看看在读模式下,读写锁是不是比互斥量更好

于互斥量我们做的改动只是将两个线程函数中的写换成读.
void *thread_1(void *arg){
        int j;
        volatile int a;
        for(j=0;j<10000000;j++){
                pthread_mutex_lock(&mutex);
                a=i;
                pthread_mutex_unlock(&mutex);
        }  
        pthread_exit((void *)0);
}

void *thread_2(void *arg){
        int j;
        volatile int b;
        for(j=0;j<10000000;j++){
                pthread_mutex_lock(&mutex);
                b=i;
                pthread_mutex_unlock(&mutex);
        }  
        pthread_exit((void *)0);
}

我们运行修改后的程序:
use mutex:
i=0

real 0m0.692s
user 0m0.660s
sys 0m0.004s

对于读写锁,我们要修改的只是把互斥量改为位读写锁就可以了
int i=0;
pthread_rwlock_t rwlock;
void *thread_1(void *arg){
        int j;
        volatile int a;
        for(j=0;j<10000000;j++){
                pthread_rwlock_rdlock(&rwlock);
                a=i;
                pthread_rwlock_unlock(&rwlock);
        }  
        pthread_exit((void *)0);
}

void *thread_2(void *arg){
        int j;
        volatile int b;
        for(j=0;j<10000000;j++){
                pthread_rwlock_rdlock(&rwlock);
                b=i;
                pthread_rwlock_unlock(&rwlock);
        }  
        pthread_exit((void *)0);
}
我们来测试一下 读写锁的性能:
输出如下:
use rwlock:
i=0

real 0m0.979s
user 0m0.916s
sys 0m0.000s


从输出中我们看到使用互斥量的程序运行速度比读写锁快。
也就是说虽然读写锁提高了并行性,但是就速度而言并不比互斥量快,在我的系统(ubuntu12.04)从上面的输出看到读写锁比互斥量明显速度方面不足。
可能这也是即使有读写锁存在还会使用互斥量的原因,因为他在速度方面略胜一筹。这就需要我们在写程序的时候
综合考虑速度和并行性并找到一个折中。

比如:  假设使用互斥量需要0.5秒,使用读写锁需要0.8秒。在类似学生管理系统这类软件中,可能百分之九十的
时间都是查询操作,那么假如现在突然来个个20个请求,如果使用的是互斥量,那么最后的那个查询请求被满足需要10后。这样,估计没人能受得了。而使用读写锁,应为读锁能够多次获得。所以所有的20个请求,每个请求都能在1秒左右
得到满足。

也就是说,在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥量,而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步

相关资料:

线程同步:互斥量与读写锁的异同与性能分析来源网络,如有侵权请告知,即处理!

编程Tags: