跟我学C++中级篇——并发中的锁的类型和分析

234 篇文章 94 订阅
5 篇文章 0 订阅

一、锁的类型

无论是在并发还是在并行开发过程中,锁机制是一个绕不开的话题。在前面分析过锁在Linux环境下本质是一个什么样的东西,也分析过一些锁的应用。今天就对锁进行一次系统的说明分析,将锁的具体的应用及相关的内在性质以及在并发等应用中的一些情况进行整体上的阐述。
在开发者眼中,锁是用来同步的,用来协调线程的执行顺序的。但其本质就是限制对资源的访问。为什么限制资源的访问?本质就是资源不足。假设一个CPU的运算速度可以无上限,为什么需要多线程共同工作导致同步问题?
锁的分类从不同的角度有不同的分类方法,最常用的就是按应用角度,可以划分为自旋锁、互斥锁和读写锁。也可以从实现的目的角度划分,如排它锁、共享锁和乐观锁以及悲观锁等等。不同的分类不代表它们是互斥的,反而有些其实是不同的叫法,比如读写锁其实就是排它锁和共享锁的另外一种称呼,经常提到的CAS就可以称之为乐观锁。还有经常提到的可重入锁等等,都是如此。
也就是说,不同的环境和语言或者框架下,可能对一些锁的称呼有所不同,但本质都有着或多或少的相同性。毕竟他们都是用来对资源进行限制笥处理的。

二、分析

锁是对资源的限制,那么哪种情况下对资源的限制最重要?一定是这种资源很重要或者说很值钱。那计算机中哪个最值钱呢?是CPU。从计算诞生到现在,编程的一个重点就是防止CPU太闲。要充分发掘CPU的潜力,让它把工作排得满满的。而理论上讲CPU的速度比这于其它资源速度要快得多,所以在访问其它资源时,如果这个活不能干了,就得锁上,告诉计算机,你先去干点别的吧(如果没得可干就歇一会儿即休眠)。而传统上开发者的认为是多个线程去同时访问一个资源,这个资源又不能为多个线程访问,所以只能用锁来对访问进行限制。弄明白了这一点,再分析资源限制就从内外两个角度明白了为什么要限制了,即为什么使用锁。
那么下面就把主要的锁分析一下,开发者经常遇到的有两个锁,一个是自旋锁一个互斥锁。前者一般在底层和一些框架上应用较多,而后者则在普遍性的应用开发上多。自旋锁一个重要的特点是不放弃对CPU资源的占有,或者说当前线程认为不会有锁或者很快会拿到锁,这也就是乐观锁。CAS使用的就是类似这种机制(循环比较并等待交换)。
互斥锁则不然,正常情况下它会如果当前线程拿不到锁会让度出CPU的占用,进入休眠状态,等待锁的释放,然后再进行唤醒执行。不过,不管是哪种锁,请一定记住,只有能一个线程拿到锁,然后执行。也就是上面提到的,资源只能有一个线程控制,即受限制于锁本身,也就是说锁的独占机制(暂时不考虑共享锁之类的情况)。
看一看内核中对自旋锁的实现:

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
#ifdef CONFIG_DEBUG_SPINLOCK
 extern void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock);
 extern int do_raw_spin_trylock(raw_spinlock_t *lock);
 extern void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock);
#else
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);
	arch_spin_lock(&lock->raw_lock);
	mmiowb_spin_lock();
}
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"
"	add	%1, %0, %4\n"
"	strex	%2, %1, [%3]\n"
"	teq	%2, #0\n"
"	bne	1b"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
	}

	smp_mb();
}

一般来说,这种贴近底层的操作都会使用汇编来实现,比之放给编译器优化会更高效。而互斥锁在前面的“futex同步机制分析”系列中有过更加详细的描述,可进行参考学习。
回头再看一下有些语言实现的类读写锁的机制,它可以理解为排它和共享锁,重点其实就是共享锁。也就是说,在读的时候儿由于数据的安全性,则可以多个线程进行读(共享锁)操作,但写因为涉及到了数据的更改,所以只能是独占的(排它锁)。在新的C++17标准里,推出了shared_mutex,基本有些类似。可以看一下其实现的相关代码:

class shared_mutex { // class for mutual exclusion shared across threads
public:
    using native_handle_type = _Smtx_t*;

    shared_mutex() noexcept // strengthened
        : _Myhandle(nullptr) {}

    ~shared_mutex() noexcept {}
    ......
    void lock() { // lock exclusive
      //重点看下面找读和写控制
    unique_lock<mutex> _Lock(_Mymtx);
    while (_Writing) {
        _Write_queue.wait(_Lock);
    }

    _Writing = true;
    while (0 < _Readers) {
        _Read_queue.wait(_Lock); // wait for writing, no readers
    }
}

_NODISCARD bool try_lock() { // try to lock exclusive
    lock_guard<mutex> _Lock(_Mymtx);
    if (_Writing || 0 < _Readers) {
        return false;
    } else { // set writing, no readers
        _Writing = true;
        return true;
    }
}
......
};

其它还有很多锁,但这些锁都是在上面这两个锁的基础上形成的。包括信号量、条件变量或者后面的内存栅栏等等。
这里需要对可重入锁进行一个简要的说明,可重入锁是指获得锁的当前线程可以无限制的再次进入锁控制的资源。这样说可能不好明白,举个例子就明白了:

#include <iostream>
#include <mutex>

std::recursive_mutex rec_mutex;

void print_func(int count) {
    rec_mutex.lock();

    if (count > 0) {
        std::cout << "count: " << count << std::endl;
        rec_mutex.unlock();
        --count;
        print_func(count);
        rec_mutex.lock();
    }

    rec_mutex.unlock();
}

int main() {
    print_func(5);
    return 0;
}

其实类似于线程自已与自己交互时,防止死锁的一种机制。
教科书和网上的资料,写得非常详尽,但也容易让开发者陷入一种细节的混沌中去。只有跳出来,才可能真正的掌握锁的应用,也就是经常说的“把厚书读薄,把薄书读厚”并且如此反复。

三、锁的优缺点

在上面分析了锁的性质进行了分析,其实锁的应用一定是两面性的。那面它的优点和缺点必然同时存在。一般来说,锁的优点是:
1、保持资源的合理使用
2、保持线程间的同步

锁的缺点:
1、最重要的一点,浪费时间,进而牺牲了资源的利用率。锁的主要功能就是保证线程的同步控制,那么一定会导致线程的切换现象,而线程的切换,往往存在有上下文的切换问题,这就是问题的所在。在多线程系列的文章中,也对此进行过说明。一个上下文的切换可能会耗费掉几微秒的时间,这对CPU来说已经很是浪费了。同时,锁机制本身也会耗费不少的时间。
2、锁使用的不当,可能引发死锁
3、锁使用的即使没有问题也有可能引发活锁
× 死锁和活锁请参考前文 :https://blog.csdn.net/fpcc/article/details/124201018

四、总结

锁的底层机制就是对资源的限制性利用,在效率和安全下寻求一个平衡。无论是现实世界还是计算机世界,资源的有限性才造就了竞争,而要让资源被安全的最大化利用,就需要一种管理机制,在现实世界,就是法律和道德;在计算机世界就是锁。
锁,其实就是计算机中的权力的象征!谁拥有了它,就可以自由的使用资源来工作。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值