作为勤勤恳恳好学的好少年,笔者决定写一篇文章来研究下锁。就好像因为饿才会去吃饭一样,使用锁也是因为锁能够解决我们需求。

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. 思考

笔者其实是想写锁和分布式锁的对比,如果放在一起的话文章太长易读性比较差,先留个坑,后面再补充一篇分布式锁的内容好了。

6. 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值