Brains


on Posix锁的认识, Linux

同步互斥 - 锁

这一篇文章主要讲述UNIX里面的锁机制,分为2类:一种主要用于多线程Thread之间的共享资源访问,另一种主要用于多进程Process之间的访问。参考资料《UNIX环境高级编程》,这是我自己的一些理解,以后还要继续补充。

线程Thread之间的锁

在多线程的环境中,涉及到共享变量的存取,就需要一定程度的同步,否则可能出现意想不到的情况。在UNIX中,用于多线程之间同步的锁分为以下几种:

  • 互斥量(mutex)
  • 读写锁(reader-writer lock)
  • 条件变量
  • 自旋锁
  • 屏障(barrier)

最基本的就是互斥量与自旋锁,其他种类的锁都是对它们的一种封装。下面我依次描述这些类型的锁。

互斥量

互斥量应该很好理解,在访问共享资源前,对互斥量进行加锁,访问完之后进行解锁。对互斥量加锁之后,其他试图再次对该互斥量加锁的线程都会进行阻塞。注意:如果加锁的线程释放了此互斥量,那么所有等待该锁的线程都会被唤醒。

在POSIX中对互斥量进行了如下描述:

#include <pthread.h>
/*
 * 互斥量使用pthread_mutex_t数据类型表示的,
 * 在使用它之前,需要对其初始化。
 */

int pthread_mutex_init( pthread_mutex_t * restrict mutex,  
                       const pthread_mutexattr_t * restrict attr );
int pthread_mutex_destory( pthread_mutex_t * mutex );

/* 成功返回0,否则返回错误编号。 */
/* 也可以这样初始化,如:*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;  

int pthread_mutex_lock( pthread_mutex_t * mutex );  
int pthread_mutex_trylock( pthread_mutex_t * mutex );   // 非阻塞版本的lock,若无法加锁,则直接返回  
int pthread_mutex_unlock( pthread_mutex_t * mutex );  

/* 设置一个定时器,如果在指定时间内还没获得该锁,即返回*/
int pthread_mutex_timedlock( pthread_mutex_t * restrict mutex,  
                            const struct timespec * restrict tsptr );


这里有2点需要补充一下:一个是restrict关键字,一个是互斥量的属性pthread_mutexattr_t。

restrict关键字:restrict是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int * restrict ptr, ptr 指向的内存单元只能被 ptr 访问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针。以上来自百度百科

pthreadmutexattrt:互斥量的属性,如果需要在多进程直接共享此互斥量,需要把属性设为PTHREAD_PROCESS_SHARED,而互斥量的健壮属性与PPTHREAD_PROCESS_SHARED有关,以后有机会接触到再深入下去,默认是PTHREAD_PROCESS_PRIVATE。

#include <pthread.h>
/* 可以通过以下接口修改此属性 */

int pthread_mutexattr_getpshared( const pthread_mutexattr_t * restrict attr,  
                                  int * restrict pshared );
int pthread_mutexattr_setpshared( pthread_mutexattr_t * attr,  
                                  int pshared );  
/* 成功返回0,失败返回错误编号 */

还有一种锁定特性,POSIX定义了4种类型:

  • PTHREAD_MUTEX_NORMAL:一种标准的互斥量类型,不做错误检查或者死锁监测。
  • PTHREAD_MUTEX_ERRORCHECK:提供错误检查
  • PTHREAD_MUTEX_RECURSIVE:递归锁,即允许同一线程在该互斥量解锁之前,继续对该互斥量解锁,很重要的属性。
  • PTHREAD_MUTEX_DEFAULT:提供默认行为,每个系统对它的实现都不同。
int pthread_mutexattr_gettype( const pthread_mutexattr_t * restrict attr,  
                              int * restrict type );
int pthread_mutexattr_settype( pthread_mutexattr_t * attr, int type );  

读写锁

读写锁与互斥量的不同在于,它区分了加锁者的意图。如果对一个共享变量读操作,则加上读锁;如对共享变量写操作,即加上写锁。同一个读写锁在加读锁的情况下,可以继续加读锁,不能加写锁。而同一个读写锁在加写锁情况下,不能再继续加任何锁。POSIX定义的接口如下:

/* 读写锁非常适用于读次数远大于写次数的情况下 */
#include <pthread.h>

int pthread_rwlock_init( pthread_rwlock_t * restrict rwlock,  
                         const pthread_rwlockattr_t * restrict attr );
int pthread_rwlock_destory( 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 );

int pthread_rwlock_tryrdlock( pthread_rwlock_t * rwlock );  
int pthread_rwlock_trywrlock( pthread_rwlock_t * rwlock );  

int pthread_rwlock_timerdlock( pthread_rwlock_t * restrict rwlock,  
                               const struct timespec * restrict tsptr );
int pthread_rwlock_timerdlock( pthread_rwlock_t * restrict rwlock,  
                               const struct timespec * restrict tsptr );

读写锁属性唯一的属性:进程共享属性。

#include <pthread.h>

int pthread_rwlockattr_init( pthread_rwlockattr_t * attr );  
int pthread_rwlockattr_destory( pthread_rwlockattr_t * attr );  

int pthread_rwlockattr_getpshared( const pthread_rwlockattr_t * restrict attr, int * restrict pshared );  
int pthread_rwlockattr_setpshared( pthread_rwlockattr_t * attr, int pshared );  

条件变量

条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身用互斥量锁住,当条件发生的时候,会发信号给等待该条件满足的线程。其主要接口如下:

#include <pthread.h>

int pthread_cond_init( pthread_cond_t * restrict cond,  
                      const pthread_condattr_t * restrict attr );
int pthread_cond_destory( pthread_cond_t * cond );  

int pthread_cond_wait( pthread_cond_t * restrict cond,  
                       pthread_mutex_t * restrict mutex );
int pthread_cond_timedwait( pthread_cond_t * restrict cond,  
                            pthread_mutex_t * restrict mutex,
                            const struct timespec * restrict tsptr );
/* 至少唤醒一个等待该条件的线程 */
int pthread_cond_signal( pthread_cond_t * cond );  
/* 唤醒所有等待该条件的线程 */
int pthread_cond_broadcast( pthread_cond_t * cond );  

其属性主要包括进程共享属性和时钟属性。

#include <pthread.h>

int pthread_condattr_init( pthread_condattr_t * attr );  
int pthread_condattr_destory( pthread_condattr_t * attr );

int pthread_condattr_getpshared( const pthread_condattr_t * restrict attr, int * restrict pshared );  
int pthread_condattr_setpshared( pthread_condattr_t * attr, int pshared );

/* 时钟属性控制着pthread_cond_timedwait采用哪个时钟 */

条件变量的使用方法一般如下:
1、锁住互斥量,调用pthread_cond_wait或者pthread_cond_timedwait将此条件变量加入到等待队列上。
2、若条件满足的情况下,由该线程调用pthread_cond_signal或者pthread_cond_broadcast唤醒等待此条件变量队列上的一个或多个线程。

自旋锁

自旋锁与互斥量类似,但它不是通过睡眠使得进程阻塞,而是在获得锁之前一直忙等,自旋锁可用于以下情况:锁被持有时间短,而且线程并不希望在重新调度上花费太多成本。

自旋锁在非抢占式内核中是非常有用的,除了提供互斥机制以外,它们还会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它们需要获取已加锁的自旋锁,因为中断处理程序不能睡眠,因此它们的同步原语只能是自旋锁。其接口如下:

#include <pthread.h>
/* 仅支持进程间共享同步这个属性 */
int pthread_spin_init( pthread_spinlock_t * lock, int pshared );  
int pthread_spin_destory( pthread_spinlock_t * lock );

int pthread_spin_lock( pthread_spinlock_t * lock );  
int pthread_spin_trylock( pthread_spinlock_t * lock );  
int pthread_spin_unlock( pthread_spinlock_t * lock );  

记住在持有自旋锁的情况下,不要调用可能会进入休眠的函数,会浪费CPU的资源。

屏障

屏障是用户协调多个线程并行工作的机制,类型于扩展版本的pthread_join函数。屏障允许多个线程阻塞,直到所有合作的线程都到达某一点,然后从该点继续执行。这种用法类似于内存屏障,它用于解决多核计算机中CPU乱序执行可能造成一些意外的错误,具体可以百度或者搜索一下。对于线程的屏障,POSIX提供了如下接口:

#include <pthread.h>

int pthread_barrier_init( pthread_barrier_t * restrict barrier,  
                          const pthread_barrierattr_t * restrict attr,
                          unsigned int count );
int pthread_barrier_destory( pthread_barrier_t * barrier );

/* 返回值是0或者PTHREAD_BARRIER_SERIAL_THREAD;否则返回错误编号 */
int pthread_barrier_wait( pthread_barrier_t * barrier );  

调用pthread_barrier_wait的线程在,屏障计数没有达到pthread_barrier_init设定的时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,满足了屏障计数,那么所有线程都会被唤醒。

关于pthread_barrier_wait的返回值:对于任意一个线程,它返回PTHREAD_BARRIER_SERIAL_THREAD,剩下的线程返回值为0。那么就可以使一个作为主线程,它可以工作在其他所有线程已完成工作的基础上。关于屏障的属性,其接口如下:

#include <pthread.h>

int pthread_barrierattr_init( pthread_barrierattr_t * attr );  
int pthread_barrierattr_destory( pthread_barrierattr_t * attr );

int pthread_barrierattr_getpshared( const pthread_barrierattr_t * restrict attr, int * restrict pshared );  
int pthread_barrierattr_setpshared( pthread_barrierattr_t * attr, int pshared );  

关于锁的思考

在加锁以及解锁的时候,为了防止死锁,一定注意每个线程的上锁顺序。使用锁是有代价的,尽量在线程中不要使用共享变量,可以使用线程特定数据TLS,这种数据对于每个线程都是私有的,具体实现见《UNIX网络编程卷1》。

还有需要注意的是,当多线程中,调用非重入的函数时,也要注意上锁。关于非重入的函数,是指该函数内部使用了静态或者全局变量。

进程Process之间的锁

关于进程之间的锁,以文件作为媒介,常用于单实例进程中,这类程序要求仅有一个实例运行。在程序刚开始运行时,需要获取锁,若获取不到就退出。所用到的锁文件一般存放在/var/run/中,后缀名为pid。

这种锁叫文件锁,又称为记录锁,以文件为媒介,可以实现对某一个文件的局部或者全局进行加锁,主要通过fctnl来实现此功能。

#include <fcntl.h>

int fcntl( int fd, int cmd, ... );  

对于记录锁,cmd是F_GETLK、F_SETLK或者F_SETLKW。第三个参数是一个指向flock结构体的指针,其定义如下:

struct flock  
{
  short l_type; /* F_RDLCK:读锁 F_WRLCK:写锁 F_UNLCK:解锁 */
  short l_whence; /* SEEK_SET:文件开头 SEEK_CUR:文件当前位置 SEEK_END:文件末尾 */
  off_t l_start; /* 与l_whence的相对偏移 */
  off_t l_len; /* 要锁内容的长度, 0 意味着锁到文件结尾 */
  pid_t l_pid; /* 返回持有该区域锁的进程pid */
}

这里需要注意的是:对于同一个进程来说,若对一个文件区间已经有了一把锁,如果又企图在同一文件区间加锁,那么新锁会替换旧锁!

关于cmd的三个类型:
F_GETLK:判断flockptr所描述的锁是否能加锁成功?如果该文件区间内有了一把锁它能阻止flockptr描述的锁请求,那么会把原本文件区间内的锁信息覆盖flockptr所指的内存区域。如果不能阻止,也是说flockptr所描述的锁能成功的加锁,那么flockptr所指的内存区域不变。

F_SETLK:设置由flockptr所描述的锁。如果我们试图获得一把锁,而兼容性规则阻止系统给我们这把锁,那么fcntl会返回-1,errno会设置EACESS或者EAGIN.

FSETLKW:是FSETLK的阻塞版本,如果该锁不能被授予,那么调用进程将睡眠。

最后

要根据合适的情形下,使用合适的锁,可以对先有的锁进行封装后使用。寒假过去了,好伤心。。。制定的假期计划也有很多没有完成。说好寒假刷题呢???结果一题都没刷!!!想了想,自己还能在学校里安心学习的日子不到4个月了,好好珍惜这剩下的日子吧,加油 ^_^

EOF

comments powered by Disqus

纸上得来终觉浅,绝知此事要躬行~