作为勤勤恳恳好学的好少年,笔者决定写一篇文章来研究下锁。就好像因为饿才会去吃饭一样,使用锁也是因为锁能够解决我们需求。
1. 为什么要使用锁
为了避免陈述什么并发资源访问、多进程、多线程共享的课本式答案,我们来举一个栗子吧。背景是:
- 现有学渣小 A,学霸小 C 两人,小 A 上课睡觉,下课后就会向小 C 借课堂笔记。
无需使用锁
- 小 A 就这么每节下课都愉快的去借阅小 C 的笔记,日子平平淡淡,小 A 还是那个小 A。
- 单进程 or 单线程 (小 A)无共享资源(笔记)访问的情况下不需要使用锁
需要使用锁
- 日子过得飞快,小 A 所在的班级迎来了转学的学渣小 B,两个学渣倒是可以玩到一块,但是问题来了,小 A 和小 B 都需要接学霸小 C 的笔记。可是小 C 的课堂笔记也是只有一份,到底借给谁呢?
- 多进程 or 多线程(小 A、小 B)访问共享资源(笔记)
2. 需要用什么锁
场景 1: 小 A 和小 B 都要抄写笔记,但是笔记只有一份,这咋办呢?
解决:谁先跟小 C 借阅笔记就先借给谁
总结:互斥锁
场景2:期中考试,并且因为迷上打游戏 小 A、小 B 都没有抄小 C 的笔记,但本着考试前都需要临时抱佛脚,这咋办呢?
解决: 反正是读笔记,善良的小 C 说,我们大家一起看一份笔记就好
总结:共享锁
场景3:腹黑的学渣小 B 想悄悄的提高自己的成绩,所以每节课下课都不停轮询小 C 要借笔记……(我劝你善良)
解决:善良的小 C 同学为了能够让小 A 也有机会抄笔记,规定被小 B 烦 10 次以上,就先借给小 A 然后借给 小 B
总结:自旋锁
3 怎么实现锁
多线程的情况下可以使用互斥锁(pthread_mutex_t ),那多进程咋办……,nginx 锁的源码了解一下。
nginx 锁采用文件锁 + 原子操作 + 信号量的方式实现。
3.1 ngx_shmtx_t 定义
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS) //原子操作
ngx_atomic_t *lock;
#if (NGX_HAVE_POSIX_SEM) //信号量
ngx_atomic_t *wait;
ngx_uint_t semaphore;
sem_t sem;
#endif
#else
ngx_fd_t fd; // 文件锁
u_char *name;
#endif
ngx_uint_t spin;
} ngx_shmtx_t
注:上述的宏表达了三个意思
- 不支持原子操作的 —> 使用文件锁
- 支持原子操作的,但不支持信号量 —> 使用原子操作
- 支持原子操作 & 信号量的 —> 二者结合使用
3.2 文件锁初始化及使用
3.2.1 文件锁初始化
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{
// 是否创建过锁
if (mtx->name) {
// 跟已创建的锁相同,直接返回,可重入的效果
if (ngx_strcmp(name, mtx->name) == 0) {
mtx->name = name;
return NGX_OK;
}
ngx_shmtx_destroy(mtx);
}
// 打开文件
mtx->fd = ngx_open_file(name, NGX_FILE_RDWR, NGX_FILE_CREATE_OR_OPEN,
NGX_FILE_DEFAULT_ACCESS);
if (mtx->fd == NGX_INVALID_FILE) {
ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_errno,
ngx_open_file_n " \"%s\" failed", name);
return NGX_ERROR;
}
// 删除文件,这步骤笔者也木有看懂,后面有空查查吧!!
if (ngx_delete_file(name) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
ngx_delete_file_n " \"%s\" failed", name);
}
mtx->name = name;
return NGX_OK;
}
3.2.2 背景知识
ngx_trylock_fd & ngx_lock_fd & ngx_unlock_fd
ngx_err_t
ngx_trylock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
//锁的类型同样是写锁
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
//操作变成了 F_SETLK, 该操作在获取不到锁时会直接返回,而不会阻塞进程。
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
ngx_err_t
ngx_lock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
//设置文件锁的类型为写锁,即互斥锁
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
//设置操作为F_SETLKW,表示获取不到文件锁时,会阻塞直到可以获取
if (fcntl(fd, F_SETLKW, &fl) == -1) {
return ngx_errno;
}
return 0;
}
ngx_err_t
ngx_unlock_fd(ngx_fd_t fd)
{
struct flock fl;
ngx_memzero(&fl, sizeof(struct flock));
//锁的类型为 F_UNLCK, 表示释放锁
fl.l_type = F_UNLCK;
fl.l_whence = SEEK_SET;
if (fcntl(fd, F_SETLK, &fl) == -1) {
return ngx_errno;
}
return 0;
}
注:文件锁实现的核心就是使用 fcntl 函数
3.2.3 lock 系列函数说明
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
ngx_err_t err;
// 调用 ngx_trylock_fd 即 fcntl(fd, F_SETLK, &fl) fl.l_type = F_WRLCK 非阻塞
err = ngx_trylock_fd(mtx->fd);
if (err == 0) {
return 1;
}
if (err == NGX_EAGAIN) {
return 0;
}
#if __osf__ /* Tru64 UNIX */
if (err == NGX_EACCES) {
return 0;
}
#endif
ngx_log_abort(err, ngx_trylock_fd_n " %s failed", mtx->name);
return 0;
}
void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{
ngx_err_t err;
// 调用 ngx_lock_fd 即 fcntl(fd, F_SETLKW, &fl) fl.l_type = F_WRLCK 阻塞
err = ngx_lock_fd(mtx->fd);
if (err == 0) {
return;
}
ngx_log_abort(err, ngx_lock_fd_n " %s failed", mtx->name);
}
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
ngx_err_t err;
// 调用 ngx_unlock_fd 即 fcntl(fd, F_SETLKW, &fl) fl.l_type = F_UNLCK 非阻塞
err = ngx_unlock_fd(mtx->fd);
if (err == 0) {
return;
}
ngx_log_abort(err, ngx_unlock_fd_n " %s failed", mtx->name);
}
3.3. 原子操作 + 信号量锁的初始化及使用
3.3.1 初始化
ngx_int_t
ngx_shmtx_create(ngx_shmtx_t *mtx, ngx_shmtx_sh_t *addr, u_char *name)
{
//mtx-lock 保存原子变量地址,锁为多进程共享,一般分配于共享内存即本例中的 addr 变量地址
mtx->lock = &addr->lock;
//决定是否自旋
// 支持信号量,-1 表示不进行自旋,即进程不会进入阻塞态
// 不支持信号量,0 或者负数表示不自旋,即调用 yield 的系统调用,进入就绪态,等待再次被调度
if (mtx->spin == (ngx_uint_t) -1) {
return NGX_OK;
}
mtx->spin = 2048;
#if (NGX_HAVE_POSIX_SEM)
mtx->wait = &addr->wait;
// 初始化信号量
// 1 表示多进程环境中
// 0 表示信号量的初始值
if (sem_init(&mtx->sem, 1, 0) == -1) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
"sem_init() failed");
} else {
mtx->semaphore = 1;
}
#endif
return NGX_OK;
}
3.3.2 背景知识
实现互斥锁的时候使用了下面两个原子操作:
// CAS 检查 lock 地址是否等于 old ,等于 lock 地址设置为 set ,返回成功,不能等于返回失败
static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old, ngx_atomic_uint_t set)
// 读取 value 地址处的变量加 add ,然后返回原来 *value 的值
static ngx_inline ngx_atomic_int_t
ngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add)
3.3.3 lock 系列函数说明
// 非阻塞锁的获取
ngx_uint_t
ngx_shmtx_trylock(ngx_shmtx_t *mtx)
{
return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
}
// 阻塞锁的获取
void
ngx_shmtx_lock(ngx_shmtx_t *mtx)
{
ngx_uint_t i, n;
ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx lock");
for ( ;; ) {
// 尝试获取锁,成功,返回;失败,继续下面的判断
if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
return;
}
// 是否为多 cpu 的环境,否的话,自旋无意义
if (ngx_ncpu > 1) {
for (n = 1; n < mtx->spin; n <<= 1) {
for (i = 0; i < n; i++) {
// 据说是为了提升循环等待的性能
ngx_cpu_pause();
}
// 再次尝试获取锁
if (*mtx->lock == 0
&& ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid))
{
return;
}
}
}
#if (NGX_HAVE_POSIX_SEM)
// 自旋次数到达上限,进入信号量条件判断
if (mtx->semaphore) {
// 增加阻塞在该信号量的进程数量
(void) ngx_atomic_fetch_add(mtx->wait, 1);
if (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid)) {
(void) ngx_atomic_fetch_add(mtx->wait, -1);
return;
}
ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
"shmtx wait %uA", *mtx->wait);
// sem_wait 阻塞在信号量上进行等待
while (sem_wait(&mtx->sem) == -1) {
ngx_err_t err;
err = ngx_errno;
if (err != NGX_EINTR) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, err,
"sem_wait() failed while waiting on shmtx");
break;
}
}
ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
"shmtx awoke");
continue;
}
#endif
// 非信号量条件下,进程主动放弃 cpu 进入就绪态,等待被调度
ngx_sched_yield();
}
}
// 锁释放
void
ngx_shmtx_unlock(ngx_shmtx_t *mtx)
{
if (mtx->spin != (ngx_uint_t) -1) {
ngx_log_debug0(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0, "shmtx unlock");
}
// 将原子变量设置为 0
if (ngx_atomic_cmp_set(mtx->lock, ngx_pid, 0)) {
// 唤醒等待在该信号量的其他进程
ngx_shmtx_wakeup(mtx);
}
}
// ngx_shmtx_wakeup 实现
static void
ngx_shmtx_wakeup(ngx_shmtx_t *mtx)
{
#if (NGX_HAVE_POSIX_SEM)
ngx_atomic_uint_t wait;
// 未启用信号量直接退出
if (!mtx->semaphore) {
return;
}
for ( ;; ) {
wait = *mtx->wait;
if ((ngx_atomic_int_t) wait <= 0) {
return;
}
// 将等待队列中的值减 1
if (ngx_atomic_cmp_set(mtx->wait, wait, wait - 1)) {
break;
}
}
ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
"shmtx wake %uA", wait);
// 将信号量的值加 1
if (sem_post(&mtx->sem) == -1) {
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno,
"sem_post() failed while wake shmtx");
}
#endif
}
大写的疑问:笔者发现这里只是对信号量进行加 1 ,具体阻塞在该信号量上的进程如何被唤醒并未说明,笔者猜测:
- 唤醒操作可能就是讲进程从阻塞态变为可运行态,等待被调度
- 信号量相关的函数使用汇编写的,笔者后面抽空仔细看下吧(流下了没有好好读书的泪水呀)
3.4 销毁锁
- 对于文件锁的话,关闭打开的文件即可
- 对于使用信号量 + 原子操作的锁,需要销毁创建的信号量
4. golang 互斥锁的实现
由于笔者 golang 比较对,就对比了下 goalng mutex 的实现,具体的见参考资料,其中比较不同的是,golang 的 mutex 加了 1ms 饥饿的判断达到相对公平的程度,翻译水平有限,直接拉注释过来:
// Mutex fairness.
//
// Mutex can be in 2 modes of operations: normal and starvation.
// In normal mode waiters are queued in FIFO order, but a woken up waiter
// does not own the mutex and competes with new arriving goroutines over
// the ownership. New arriving goroutines have an advantage -- they are
// already running on CPU and there can be lots of them, so a woken up
// waiter has good chances of losing. In such case it is queued at front
// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
// it switches mutex to the starvation mode.
//
// In starvation mode ownership of the mutex is directly handed off from
// the unlocking goroutine to the waiter at the front of the queue.
// New arriving goroutines don't try to acquire the mutex even if it appears
// to be unlocked, and don't try to spin. Instead they queue themselves at
// the tail of the wait queue.
//
// If a waiter receives ownership of the mutex and sees that either
// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
// it switches mutex back to normal operation mode.
//
// Normal mode has considerably better performance as a goroutine can acquire
// a mutex several times in a row even if there are blocked waiters.
// Starvation mode is important to prevent pathological cases of tail latency.
5. 思考
笔者其实是想写锁和分布式锁的对比,如果放在一起的话文章太长易读性比较差,先留个坑,后面再补充一篇分布式锁的内容好了。