这一篇文章主要讲述UNIX里面的锁机制,分为2类:一种主要用于多线程Thread之间的共享资源访问,另一种主要用于多进程Process之间的访问。参考资料《UNIX环境高级编程》,这是我自己的一些理解,以后还要继续补充。
线程Thread之间的锁
在多线程的环境中,涉及到共享变量的存取,就需要一定程度的同步,否则可能出现意想不到的情况。在UNIX中,用于多线程之间同步的锁分为以下几种:
- 互斥量(mutex)
- 读写锁(reader-writer lock)
- 条件变量
- 自旋锁
- 屏障(barrier)
最基本的就是互斥量与自旋锁,其他种类的锁都是对它们的一种封装。下面我依次描述这些类型的锁。
互斥量
互斥量应该很好理解,在访问共享资源前,对互斥量进行加锁,访问完之后进行解锁。对互斥量加锁之后,其他试图再次对该互斥量加锁的线程都会进行阻塞。注意:如果加锁的线程释放了此互斥量,那么所有等待该锁的线程都会被唤醒。
在POSIX中对互斥量进行了如下描述:
1 |
|
还有一种锁定特性,POSIX定义了4种类型:
- PTHREAD_MUTEX_NORMAL: 一种标准的互斥量类型,不做错误检查或者死锁监测。
- PTHREAD_MUTEX_ERRORCHECK: 提供错误检查
- PTHREAD_MUTEX_RECURSIVE: 递归锁,即允许同一线程在该互斥量解锁之前,继续对该互斥量解锁,很重要的属性。
- PTHREAD_MUTEX_DEFAULT: 提供默认行为,每个系统对它的实现都不同。
1 | int pthread_mutexattr_gettype( const pthread_mutexattr_t * restrict attr, |
读写锁
读写锁与互斥量的不同在于,它区分了加锁者的意图。如果对一个共享变量读操作,则加上读锁;如对共享变量写操作,即加上写锁。同一个读写锁在加读锁的情况下,可以继续加读锁,不能加写锁。而同一个读写锁在加写锁情况下,不能再继续加任何锁。POSIX定义的接口如下:
1 | /* 读写锁非常适用于读次数远大于写次数的情况下 */ |
读写锁属性唯一的属性:进程共享属性。
1 |
|
条件变量
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身用互斥量锁住,当条件发生的时候,会发信号给等待该条件满足的线程。其主要接口如下:
1 |
|
其属性主要包括进程共享属性和时钟属性。
1 |
|
条件变量的使用方法一般如下:
1、锁住互斥量,调用pthread_cond_wait或者pthread_cond_timedwait将此条件变量加入到等待队列上。
2、若条件满足的情况下,由该线程调用pthread_cond_signal或者pthread_cond_broadcast唤醒等待此条件变量队列上的一个或多个线程。
自旋锁
自旋锁与互斥量类似,但它不是通过睡眠使得进程阻塞,而是在获得锁之前一直忙等,自旋锁可用于以下情况:锁被持有时间短,而且线程并不希望在重新调度上花费太多成本。
自旋锁在非抢占式内核中是非常有用的,除了提供互斥机制以外,它们还会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它们需要获取已加锁的自旋锁,因为中断处理程序不能睡眠,因此它们的同步原语只能是自旋锁。其接口如下:
1 |
|
记住在持有自旋锁的情况下,不要调用可能会进入休眠的函数,会浪费CPU的资源。
屏障
屏障是用户协调多个线程并行工作的机制,类型于扩展版本的pthread_join函数。屏障允许多个线程阻塞,直到所有合作的线程都到达某一点,然后从该点继续执行。这种用法类似于内存屏障,它用于解决多核计算机中CPU乱序执行可能造成一些意外的错误,具体可以百度或者搜索一下。对于线程的屏障,POSIX提供了如下接口:
1 |
|
调用pthread_barrier_wait的线程在,屏障计数没有达到pthread_barrier_init设定的时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,满足了屏障计数,那么所有线程都会被唤醒。
关于pthread_barrier_wait的返回值:对于任意一个线程,它返回PTHREAD_BARRIER_SERIAL_THREAD,剩下的线程返回值为0。那么就可以使一个作为主线程,它可以工作在其他所有线程已完成工作的基础上。关于屏障的属性,其接口如下:
1 |
|
关于锁的思考
在加锁以及解锁的时候,为了防止死锁,一定注意每个线程的上锁顺序。使用锁是有代价的,尽量在线程中不要使用共享变量,可以使用线程特定数据TLS,这种数据对于每个线程都是私有的,具体实现见《UNIX网络编程卷1》。
还有需要注意的是,当多线程中,调用非重入的函数时,也要注意上锁。关于非重入的函数,是指该函数内部使用了静态或者全局变量。
进程Process之间的锁
关于进程之间的锁,以文件作为媒介,常用于单实例进程中,这类程序要求仅有一个实例运行。在程序刚开始运行时,需要获取锁,若获取不到就退出。所用到的锁文件一般存放在/var/run/中,后缀名为pid。
这种锁叫文件锁,又称为记录锁,以文件为媒介,可以实现对某一个文件的局部或者全局进行加锁,主要通过fctnl来实现此功能。
1 |
|
对于记录锁,cmd是F_GETLK、F_SETLK或者F_SETLKW。第三个参数是一个指向flock结构体的指针,其定义如下:
1 | struct flock { |
这里需要注意的是:对于同一个进程来说,若对一个文件区间已经有了一把锁,如果又企图在同一文件区间加锁,那么新锁会替换旧锁!
关于cmd的三个类型:
-
F_GETLK: 判断flockptr所描述的锁是否能加锁成功?如果该文件区间内有了一把锁它能阻止flockptr描述的锁请求,那么会把原本文件区间内的锁信息覆盖flockptr所指的内存区域。如果不能阻止,也是说flockptr所描述的锁能成功的加锁,那么flockptr所指的内存区域不变。
-
**F_SETLK:**设置由flockptr所描述的锁。如果我们试图获得一把锁,而兼容性规则阻止系统给我们这把锁,那么fcntl会返回-1,errno会设置EACESS或者EAGIN.
-
F_SETLKW: 是F_SETLK的阻塞版本,如果该锁不能被授予,那么调用进程将睡眠。
最后
要根据合适的情形下,使用合适的锁,可以对先有的锁进行封装后使用。寒假过去了,好伤心。。。制定的假期计划也有很多没有完成。说好寒假刷题呢???结果一题都没刷!!!想了想,自己还能在学校里安心学习的日子不到4个月了,好好珍惜这剩下的日子吧,加油 _