iOS-机制

iOS-锁

2020-03-13  本文已影响0人  xxxxxxxx_123

前言

有这么一个经典的案例,某一趟车次现在还有20张余票,火车站有4个售票窗口,人们开始排队买票。我们来模拟一下这个场景。

@property (nonatomic, assign) NSUInteger ticketCount;

- (void)saleAllTickets {
    self.ticketCount = 20;

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self showTicketState];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self showTicketState];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            [self showTicketState];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self showTicketState];
        }
    });
}

- (void)showTicketState {
    if (self.ticketCount > 0) {
        self.ticketCount--;
        sleep(0.1);
        NSLog(@"当前余票还剩:%ld张",self.ticketCount);
    } else {
        NSLog(@"当前车票已售罄");
    }
}

咋一看程序没有问题,运行程序:

image

可以看出,程序的运行结果并不是我们想要的,因为每个窗口的卖票都是独立且同时进行的,所以showTicketState这个方法同时可能被多个线程访问,这样线程的不安全导致了数据的异常。解决这个问题就引入了我们今天要探究的主题---锁,当我们用锁锁住showTicketState这个方法,保证其线程安全,程序才能正常运行。

基本概念

那么在iOS开发中锁是什么呢?顾名思义,锁是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(acquire)锁,并在访问结束之后释放(release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

多线程编程时,当多个线程同时对一块内存发生读和写的操作,可能就会出现程序执行的顺序会被打乱,造成提前释放一个变量,计算结果错误等情况。所以我们需要使用线程安全工具---也就是锁将线程不安全的代码“锁”起来,保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的同步(synchronization)访问 。

借用锁的功能,我们就可以修改上面的代码,让其正运行,比如使用@synchronized,或者是使用信号量dispatch_semaphore_tshowTicketState方法锁住,让其在多线程访问的时候依然同步执行。

锁的分类

根据锁的状态,锁的特性等我们可以对锁进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁的不同实现。

iOS中的锁大致可以分为互斥锁、自旋锁、信号量这三种。

互斥锁

互斥锁,顾名思义,互相排斥。是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成线程安全的目的。当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。

互斥锁由POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化的。

  1. 静态初始化互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  1. 动态初始化互斥量
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
  1. 注销毁互斥量
int pthread_mutex_destory(pthread_mutex_t *mutex)
  1. 获取锁(加锁)

当另一个线程来获取这个锁的时候,发现这个锁已经加锁,那么这个线程就会进入休眠状态,直到这个互斥量被解锁,线程才会重新被唤醒。

int pthread_mutex_lock(pthread_mutex_t *mutex)
  1. 尝试获取锁
    当互斥量已经被锁住时调用该函数将返回错误代码EBUSY,如果当前互斥量没有被锁,则会正常加锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex)
  1. 释放锁(解锁)
int pthread_mutex_unlock(pthread_mutex_t *mutex)

同一条线程如果连续锁定两次或者多次(递归),就会造成死锁问题。那如果想在递归中使用锁,那要怎么办呢,这就用到了NSRecursiveLock递归锁,而递归锁也是互斥锁的一种。递归锁可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁。

互斥锁分为递归锁和非递归锁。常见的递归锁有NSRecursiveLock;对象锁@synchronized。常见的非递归锁有pthread_mutexNSLock;条件锁NSConditionNSConditionLock

对象锁@synchronized,是基于recursive_mutex_t的上层封装,属于递归锁。当我们对其传入nil的时候,它不会做任何事情,可以用来防止死递归。让@synchronized具备处理递归能力的是lockCount,让其能够处理多线程的是threadCount。在日常开发中,要慎用@synchronized(self),直接将self传入@synchronized确实是很简单粗暴的方法,但是这样容易导致死锁的出现。

条件锁NSConditionNSConditionLock就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是被锁住了。当资源被分配到了,条件锁打开,进程继续运行。NSCondition是基于对pthread_mutex的封装,而NSConditionLock是对NSCondition做了一层封装。

自旋锁

线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

简单的说,线程1获取到锁,在释放锁之前,线程2又来获取锁,此时是获取不到的,线程2会不断的进入循环,一直检查锁是否已被释放,如果释放,则能获取到锁。

OSSpinLock之前是一种很具有代表性的自旋锁,后来因为安全性问题已经被苹果废弃,此处就不再多做赘述。

我们常说的读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成“读者”和“写者”,“读者”只对共享资源进行读访问,“写者”则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁,也就是我们常说的多读单写。正是因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须直到所有的线程释放锁。读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。

鉴于读写锁的特性:

读的时候:

dispatch_sync(concurrentQueue, ^{
    
}

写的时候:

dispatch_barrier_sync(concurrentQueue, ^{
    
});

其实自旋锁,也是互斥锁的一种实现,而两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。区别在于调度机制上有所不同。大多数情况下,自旋锁看起来是比较耗费cpu的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。

互斥锁和自旋锁的主要区别如下:

信号量(dispatch_semaphore)

是一种更高级的同步机制,互斥锁可以说是
dispatch_semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不仅仅是线程间互斥。

dispatch_semaphore_wait的参数为0的时候会堵塞线程,起到锁的作用。它能够通过创建信号量的值控制最大并发数,是性能最高的锁。

信号量和互斥锁的区别:

此处附上网上一张性能图对比作为结束:

image

具体锁的使用请见下几篇文章。

上一篇下一篇

猜你喜欢

热点阅读