多线程同步的锁:互斥锁和读写锁

在 -maimai 上读到去阿里的同学考察多线程的锁问题,想起早几年写的一个用Java实现的Session Server(现在还在服役中),起初是基于Java的Synchronized用在方法和对象上,后来改由ConcurrentHashMap做容器。参照 -ChinaUnix 上,ifndef 给例子(-R/92TQ ),我重复了一下这个实验,算是对互斥锁和读写锁的一种回顾。

概念,“线程同步简单的说就是当多个线程共享相同的内存时,当某个线程可以修改变量,而其他线程也可以读取或修改这个变量的时候,就需要对这些线程进行同步,以确保他们在访问变量的存储内容时不会访问到无效的数值。”,确保同步的措施是在操作前进行加锁。

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

读写锁, “当读写锁是写加锁时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁是读加锁时,在这个锁被解锁之前,所有试图以读模式对他进行加锁的线程都可以得到访问权”。

根据如上描述,以及Java SDK中对 Synchronized和ConcurrentHashMap的描述,

Synchronized,“ it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.”(-R/P2Tc )

ConcurrentHashMap,” even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. ”(-R/22Uk ) …“It uses a multitude of locks, each lock controls one segment of the hash. When setting data in a particular segment, the lock for that segment is obtained.”(-R/b2Tc )

从上文的描述来看,Synchronized的实现类似互斥锁,而ConcurrentHashMap的实现类似于读写锁。

实际的例子,若多线程不加锁,运行的结果将很异常,如下程序:

/*
 * pthread mutex lock, rwlock test
* wadelau@ufqi.com, Mon Apr  4 21:32:27 CST 2016
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
int i = 0;
static int COUNT_NUM = 1000000;
static void *thread_1();
static void *thread_2();
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(0);
}
err = pthread_join(th2, NULL);
if( err != 0){
printf(“Wait thread done error: %s\n”, strerror(err));
exit(0);
}
printf(“i=%d\n”, i);
exit(0);
}
static void *thread_1(void *args){
int j;
for(j=0; j<COUNT_NUM; j++){
i++;
}
pthread_exit((void *)0);
}
static void *thread_2(void *args){
int j;
for(j=0; j<COUNT_NUM; j++){
i++;
}
pthread_exit((void *)0);
}

编译时,使用:

shell> gcc -lpthread xxx.c  -o xxx.out

程序运行十次的结果各不相同:

shell> for i in {0..9}; do ./pthread_nolock.out; done;

i=1042904
i=1064909
i=1026858
i=1038045
i=1133655
i=1085719
i=1029224
i=1089466
i=1020473
i=1041702

对线程使用互斥锁:

static int COUNT_NUM = 1000000;
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
err = pthread_create(&th1, NULL, thread_1, (void *)0);
pthread_mutex_destroy(&mutex);
printf(“i=%d\n”, i);
for(j=0; j<COUNT_NUM; j++){
pthread_mutex_lock(&mutex);
i++;
pthread_mutex_unlock(&mutex);
}

 

再次运行的结果:

shell> for i in {0..9}; do ./pthread_mutex.out ; done;
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000
i=2000000

已经完全符合预期了。 再对线程使用读写锁。代码变化:

static int COUNT_NUM = 1000000;
//pthread_mutex_t mutex;
pthread_rwlock_t rwlock;
//pthread_mutex_init(&mutex, NULL);
pthread_rwlock_init(&rwlock, NULL);
err = pthread_create(&th1, NULL, thread_1, (void *)0);
//pthread_mutex_destroy(&mutex);
pthread_rwlock_destroy(&rwlock);
printf(“i=%d\n”, i);
 volatile int a;
 for(j=0; j<COUNT_NUM; j++){
        pthread_rwlock_rdlock(&rwlock);
        a = i;
        pthread_rwlock_rdlock(&rwlock);
 }

再次运行程序,并计时,看看读写锁的程序时间:

shell> for i in {0..9}; do time ./pthread_rwlock_rd.out ; done;
i=1000000
real    0m0.534s
user    0m0.576s
sys     0m0.460s
i=1000000
real    0m0.333s
user    0m0.392s
sys     0m0.232s
i=1000000
real    0m0.282s
user    0m0.340s
sys     0m0.180s
i=1000000
real    0m0.336s
user    0m0.340s
sys     0m0.288s
i=1000000
real    0m0.278s
user    0m0.324s
sys     0m0.188s
i=1000000
real    0m0.224s
user    0m0.268s
sys     0m0.124s
i=1000000
real    0m0.257s
user    0m0.272s
sys     0m0.192s
i=1000000
real    0m0.261s
user    0m0.296s
sys     0m0.176s
i=1000000
real    0m0.258s
user    0m0.256s
sys     0m0.212s
i=1000000
real    0m0.366s
user    0m0.420s
sys     0m0.272s

 

同时运行使用互斥锁程序并计时,数据如下:

shell> for i in {0..9}; do time ./pthread_mutex_rd.out ; done;
i=2000000
real    0m0.178s
user    0m0.168s
sys     0m0.140s
i=2000000
real    0m0.183s
user    0m0.176s
sys     0m0.140s
i=2000000
real    0m0.177s
user    0m0.188s
sys     0m0.120s
i=2000000
real    0m0.183s
user    0m0.168s
sys     0m0.152s
i=2000000
real    0m0.198s
user    0m0.152s
sys     0m0.196s
i=2000000
real    0m0.168s
user    0m0.124s
sys     0m0.160s
i=2000000
real    0m0.155s
user    0m0.160s
sys     0m0.100s
i=2000000
real    0m0.165s
user    0m0.148s
sys     0m0.136s
i=2000000
real    0m0.160s
user    0m0.140s
sys     0m0.132s
i=2000000
real    0m0.136s
user    0m0.152s
sys     0m0.080s

 

可以发现,读写锁比互斥锁的运行时间要长。然而也应该辩证的看,在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥量,而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步。

进一步地,在前文所述的 Session Server中,由使用互斥锁的Synchronized的更改为使用读写锁(读不上锁,写部分区间上锁)的ConcurrentHashMap,算是某种优化。

最后,想陈述的是某种编码风格,code style:

1) 逻辑区块用单行隔开,single blank line for every block
code-style-201604

2)功能区块用双空格,double blank lines for every function block
code-style-201604-002

3)左花括号 { 随上行尾部,右花括号 } 换行顶格, else, else if 换新行.

4)注释要写,能用English or pinyin,不用其他语言。

5)能不写常量字符,尽量不要写。从一开始就这么老师要求学生,每每总是抑制不住写一些常量值。

One more,起初重复运行程序十遍,我用了Awk,没有原因,就是顺手:
shell> echo “” | awk ‘{print “time ./pthread_mutex_rd.out “;}’ |sh

This entry was posted in 编程技术, 计算机技术 and tagged , , , , , . Bookmark the permalink.

发表评论

电子邮件地址不会被公开。 必填项已用*标注