您现在的位置是:首页 >学无止境 >【iOS_锁】网站首页学无止境

【iOS_锁】

神奇阿道和小司 2024-06-24 12:01:01
简介【iOS_锁】

前言

上周学习了iOS的另一个多线程Operation Queue,这就不得不提到锁了。

在iOS开发里面,锁住要是为了保护共享资源的访问确保线程安全性和避免竞争条件。iOS的应用通常在多线程的环境下运行,之前学习的多线程GCD和Operation Queue都是执行并发任务的,多个线程可能同时访问某个对象,所以锁可以确保每次只有一个线程能够修改或访问共享资源,保护了数据的安全,避免了资源冲突。

比如有时候需要确保线程的执行顺序或者多个线程之间的协调,锁可以用来实现同步机制,操作系统的学习就介绍了最常用的互斥锁实现线程的等待和唤醒实现线程间的通信和同步。

在看源码的过程中有时也会见到一些基础锁的操作,这里学习iOS的锁为之后的源码作揖铺垫。

锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

⚠️:不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。

线程安全

当一个线程访问数据的时候,其他线程不能对其进行访问,直到该线程访问完毕。简单来说就是某个时刻的某个数据只能被一个线程操作。

  • 数据安全:当同一时刻多个线程对某个数据进行访问的时候,就会造成数据的不安全。
  • 避免资源冲突:某些操作可能需要独占访问某个资源,例如文件或网络连接。通过使用锁,可以确保同一时间只有一个线程能够使用该资源,避免冲突和争用。
  • 线程同步:有时候需要确保线程的执行顺序或协调多个线程之间的操作。锁可以用于实现同步机制,例如使用互斥锁来实现线程的等待和唤醒,或者使用条件变量来实现线程间的通信和同步。

锁?的作用

引入经典的卖票问题

我们模拟了两个窗口同时卖票,为了实现同时卖票 dispatch_async 异步执行.

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self startSell];

}
//记录共售出多少票的全局变量
int cnt = 0;

- (void)startSell {
    //一共有50张票
    self.ticketSurplusCount = 50;
       
    __weak typeof (self) weakSelf = self;
        
    //一号售票窗口
    // dispatch_async 异步执行
    // 全局并发队列(Global Dispatch Queue)
    // DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 50; ++i) {
            [weakSelf saleTicketSafe];
        }
    });
        
    //二号售票窗口
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 50; ++i) {
            [weakSelf saleTicketSafe];
        }
    });
}

//售票的方法
- (int)saleTicketSafe {
    while (1) {
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
            return 0;
        }
    }
}


@end

打印结果请添加图片描述
再没有加锁的情况下,cnt不断的++,我们只有50张票。结果卖出了55张。

这个就是我们说的数据安全出现问题的情况,当同一时刻多个线程对某个数据进行访问的时候,就会造成数据的不安全。

锁的种类

请添加图片描述
在iOS中锁的基本种类只有两种:互斥锁、自旋锁,其他的比如条件锁、递归锁、信号量都是上层的封装和实现。

互斥锁 自旋锁

  • 互斥锁:保证在加锁的每个时刻,都只有一个线程访问对象。当获取锁的操作失败的时候,线程进入睡眠等待锁被释放的时候唤醒线程
  • 自旋锁自旋锁不会引起调用者的睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环尝试,直到该自旋锁的保持者已经释放了锁(忙等待)。因为不会引起调用者睡眠,所以效率高于互斥锁

加锁原理

  • 互斥锁的原理是在进入临界区前,获取锁并占用它,其他线程需要进入临界区时会被阻塞,直到该线程释放锁为止。互斥锁可以保证只有一个线程能够访问共享资源,从而避免了竞态条件的发生。
  • 自旋锁的原理则是在获取锁失败后,不断地尝试获取锁,而不是立即阻塞等待。这种方式通常适用于临界区持锁时间短暂的场景,因为如果临界区持锁时间过长,其他线程会一直在等待获取锁,造成系统资源浪费。

缺点对比

自旋锁的缺点
  • 调用者在未获得锁的情况下,一直运行--自旋,所以占用着CPU资源,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁
互斥锁的缺点

相比之下,互斥锁的实现需要操作系统支持,因为它需要进行上下文切换和阻塞唤醒等操作,对系统开销较大。而自旋锁则更加轻量级,但是在高并发场景下容易造成CPU资源浪费。

互斥锁更适合长时间的临界区保护,而自旋锁则更适合短时间的临界区保护。

各种锁

OSSpinLock

OSSpinLock是苹果的macOS和iOS操作系统提供的一种同步原语。它用于保护关键代码段不被多个线程并发访问。

OSSpinLock的基本思想是,**一次只能由一个线程持有它,其他试图获取锁的线程将在一个循环中自旋,直到锁变得可用。这种自旋行为旨在避免将等待线程置于睡眠状态并在锁变得可用时再次唤醒它们的开销。**所以该锁是自旋锁的一种

由于自旋可能会导致高争用场景下的性能降低,因此自macOS 10.12和iOS 10.0起,OSSpinLock已经被弃用。相反,苹果建议使用os_unfair_lock或dispatch_semaphore作为替代方案。

使用OSSpinLock

引入头文件

#import <libkern/OSAtomic.h>

操作

// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

按照锁的思想,我们应该保护的是卖票的过程,当有一个窗口在访问的时候为总的售票方法加锁,当卖出一次票就解锁,保证了在卖票的过程仅仅有一个窗口及时访问

@property (nonatomic, assign) OSSpinLock spinLock;
//售票的方法
- (int)saleTicketSafe {
    while (1) {
        OSSpinLockLock(&_spinLock);
//        [self.spinLock lock];
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
            return 0;
        }
        OSSpinLockUnlock(&_spinLock);

    }
}

看下打印结果和之前不加锁的对比,不加锁的情况下不仅是多个混乱的线程访问,并且卖的票数也不对
请添加图片描述
使用锁即完成了我们单个线程访问数据的效果也不会出现错乱。
请添加图片描述

在这我想用[self.spinLoack lock] 但是报错了Bad receiver type 'OSSpinLock' (aka 'int')

请添加图片描述请添加图片描述
NSLock 并不是一个自旋锁。它内部实现了 POSIX 线程中提供的 pthread_mutex,默认情况下是使用互斥锁实现的,而非自旋锁。因此,[self.spinLock lock] 方法会将当前线程阻塞,直到获取到锁为止,这与自旋锁的语义不同。

OSSpinLock存在缺陷

  • OSSpinLock不会记录持有它的线程信息,当发生优先级反转的时候,系统找不到低优先级的线程,导致系统可能无法通过提高优先级解决优先级反转问题
  • 高优先级线程使用自旋锁忙等待的时候一直在占用CPU时间片,导致低优先级线程拿到时间片的概率降低。

互斥锁分为两种: 递归锁、非递归锁

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

对于递归锁我们要注意使用时死锁问题,前后代码相互等待就会死锁
对于非递归锁,我们强行使用递归就会造成堵塞。

os_unfair_lock 【非递归互斥锁】

os_unfair_lock是互斥锁里的非递归互斥锁。

苹果采用os_unfair_lock来代替不安全的OSSpinLock,且由于os_unfair_lock会休眠而不是忙等,所以属于 互斥锁 ,且是非递归互斥锁

锁的修饰

提及一点,锁是不能用strong修饰的:锁(如 os_unfair_lock)不是对象,而是一种线程同步机制,因此不能使用强引用修饰。

锁是一个结构体或原始数据类型,它没有引用计数(reference count)或生命周期。使用强引用修饰锁属性是无效的,因为锁不会被 retain 和 release。相反,锁的生命周期由程序员显式控制。
请添加图片描述

@property (nonatomic, assign) os_unfair_lock unfairLock;

在上述代码中,使用assign关键字修饰os_unfair_lock属性,表明这是一个简单的赋值操作,而不涉及引用计数或内存管理。这是因为os_unfair_lock是一种原始数据类型,可以直接赋值给属性,而不需要进行引用计数操作。

使用

初始化

 self.unfairLock = (OS_UNFAIR_LOCK_INIT);
//售票的方法
- (void)saleTicketSafe {
    while (1) {
        //加锁
        os_unfair_lock_lock(&_unfairLock);
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
            //解锁
            // 成对出现
            os_unfair_lock_unlock(&_unfairLock);
            break;
        }
        //解锁
        os_unfair_lock_unlock(&_unfairLock);
    }
}

请添加图片描述
关于它的定义:

在这里插入图片描述

os_unfair_lock是在iOS10之后为了替代自旋锁OSSpinLock而诞生的,主要是通过线程休眠的方式来继续加锁啊(自旋锁),而不是一个“忙等”的锁。猜测是为了解决自旋锁的优先级反转的问题。

自旋锁的优先级反转的问题

自旋锁存在一个优先级反转(priority inversion)的问题,这是一种潜在的并发编程问题。优先级反转指的是当一个高优先级的线程需要获取一个被低优先级线程持有的锁时,高优先级线程会被阻塞等待低优先级线程释放锁,从而导致整个系统的响应性降低。

一个示例来说明优先级反转的问题:

假设有三个线程:线程 A(高优先级)、线程 B(中优先级)和线程 C(低优先级),它们都需要使用同一个自旋锁。

  • 线程 A 需要获取自旋锁,但此时锁已经被线程 B(中优先级)持有。
  • 根据自旋锁的特性,线程 A 将一直处于忙等状态,不断尝试获取锁。
  • 在此期间,线程 C(低优先级)开始运行,并且不需要使用该锁。
  • 线程 C 一直运行,而线程 B 由于某些原因暂停执行。
  • 线程 A(高优先级)不断尝试获取锁,但由于线程 C(低优先级)一直在运行,它无法获得锁,导致整个系统的响应性降低。此时,线程 C 占据了锁的资源,但实际上它并不需要使用锁

解决方案:优先级反转的解决方案之一是优先级继承(priority inheritance),即在发生优先级反转时,将低优先级线程的优先级提升到高优先级线程的优先级,以确保高优先级线程能够尽快获取锁并完成任务。这种方法可以减少优先级反转的影响,但需要在操作系统或编程框架中支持。

另一种解决方案是优先级屏蔽(priority ceiling),即为锁分配一个优先级上限,当低优先级线程持有锁时,它会自动升级到锁的优先级,从而防止高优先级线程被阻塞。

pthread_mutex:【互斥锁本身】

  • 它是互斥锁:本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠,另外pthread_mutex也是非递归的锁。
    pthread_mutex 是 POSIX 线程库提供的一种互斥锁(mutex)。它是一种常用的线程同步机制,用于保护共享资源的访问,以确保线程安全性和避免竞争条件。

pthread_mutex 是跨平台的,可在多个操作系统上使用,包括 macOSLinux 和其他遵循 POSIX 标准的系统。

NSLock、NSCondtion、NSRecursiveLock等锁其实都是对pthread锁进行了一层封装,之后解析源码的时候会学到

头文件 <pthread.h>

// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁 
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);

需要释放该锁 pthread_mutex_destroy(&_lock);

//售票的方法
- (void)saleTicketSafe {
    while (1) {
        // 加锁
        pthread_mutex_lock(&_lock);
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
            // 解锁
            pthread_mutex_unlock(&_lock);
            // 释放锁
            pthread_mutex_destroy(&_lock);
            break;
        }
        // 解锁
        pthread_mutex_unlock(&_lock);
    }
}

NSLock 【非递归互斥锁】

NSLock 是 Foundation 框架提供的一种互斥锁类,用于实现线程同步和保护共享资源的访问。它是在 Objective-C 中使用的锁机制,适用于 macOS 和 iOS 开发。

使用 NSLock 可以确保在同一时间只有一个线程能够访问被锁定的临界区域,从而保证线程安全性和避免竞争条件。

NSLock基于互斥锁pthroad_mutex封装而来,是一把互斥非递归锁

下面是使用 NSLock 的示例代码:

#import <Foundation/Foundation.h>

NSLock *lock = [[NSLock alloc] init];

void threadFunction(void *arg) {
    // 加锁
    [lock lock];

    // 临界区操作

    // 解锁
    [lock unlock];
}

创建了一个 NSLock 对象 lock,然后在需要保护的临界区域前后使用 [lock lock][lock unlock] 分别进行加锁和解锁操作。

此外,NSLock 也提供了 tryLocklockBeforeDate: 等方法,用于尝试获取锁或在指定时间内获取锁。这些方法在特定的场景下可能更加灵活地满足需求。

在使用 NSLock 时,要确保加锁和解锁操作成对出现(注意:-lock和-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用,否则会产生未知结果。),避免死锁和资源泄漏等问题。同时,要避免长时间持有锁或嵌套锁的使用,以避免性能问题和潜在的竞争条件。

对非递归锁强行使用递归

NSLock是非递归锁,不能重入,否则会发生死锁请添加图片描述
在同一线程上调用两次NSLocklock方法将会永久锁定线程。同时也重点提醒向NSLock对象发生解锁消息时,必须确保消息时从发送初始锁定消息的同一个线程发送的,否则就会产生未知问题。

多次重入NSLock导致线程阻塞

请添加图片描述
请添加图片描述

打印解释:上面的代码最终只打印了testLock1,其他的几个打印不会去执行。因为 testLock1被锁了之后,还没有调用解锁就执行了testLock2。这个时候去lock 但是锁获取不到就休眠等待,直到testLock1 unlock解锁之后才会继续执行,但是这个时候testLock2 不执行完, testLock1 里面的代码也就被卡着不能继续

NSLock的实现

通过查看 NSLock的 API,发现是在Foundation框架下的如图:
在这里插入图片描述

NSLock也是基于pthread_mutex封装的锁。

通过查看 API 发现,锁都是遵循了一个叫做NSLocking的协议,所以大部分锁都是有如下两个方法的:
请添加图片描述

@protocol NSLocking
- (void)lock;
- (void)unlock;
- @end

对于OC的Foundation一直未开源 过 swift的Foundation框架的开源源码来看看锁是如何封装实现的。

  • NSLock
    在这里插入图片描述
    从 swift的Foundation的源码工程中搜索NSLocking协议,发现这里针对不同的平台进行了一些设置和初始化工作,很明显可以看出来是对pthread的封装。
    还有构造方法(init)、析构方法(deinit)、加锁(lock)、解锁(unlock)的方法定义,如下:
    在这里插入图片描述
  • 构造方法init()就是调用了pthread的pthread_mutex_init(mutex, nil)方法
  • 析构方法deinit就是调用了pthread的pthread_mutex_destroy(mutex)方法
  • 加锁方法 lock()就是调用了pthread的pthread_mutex_lock(mutex)方法
  • 解锁方法 unlock()就是调用了pthread的pthread_mutex_unlock(mutex)方法、其根本是对pthread_mutex的封装,在pthread_mutex中可以通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))来设置锁为递归锁,这里并没有设置,所以在使用的时候需要注意NSLock并不能在递归函数中使用。(在之后的RecursiveLock里面会涉及到这个方法)

NSRecursiveLock 【递归互斥锁-非多线程特性】

NSRecursiveLock 是 Foundation 框架提供的一种可递归使用的互斥锁类。它是 NSLock 的子类,具有相同的基本功能,但允许同一个线程多次对锁进行加锁操作而不会造成死锁。

NSRecursiveLock虽然有递归性,但是不支持多线程的递归加锁

使用:

#import <Foundation/Foundation.h>

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

void recursiveFunction(int count) {
    // 加锁
    [lock lock];

    if (count > 0) {
        NSLog(@"Count: %d", count);
        recursiveFunction(count - 1);
    }

    // 解锁
    [lock unlock];
}

我们创建了一个 NSRecursiveLock 对象 lock,然后在递归函数 recursiveFunction 中使用该锁。函数会在每次递归调用前后分别进行加锁和解锁操作,保证临界区的线程安全性。

递归锁多次加锁

之前讲过,如果NSLock lock了之后,没有unlock那么会发生死锁。

那么NSRecursiveLock lock之后,没有unlock,会发生什么呢?
请添加图片描述

同一线程可以多次获取而不会导致死锁的锁,重点是在同一线程。

注意:

  • NSLock如果没有在lock之后unlock,那么会被死锁
  • NSRecursiveLock,在这一点优化了不少,在单一线程中,重复lock也不会影响该线程。但是没有及时unlock,是会导致其他线程阻塞的。

递归锁的死锁问题

前后代码相互等待造成死锁的情况,通常称为死锁(Deadlock)。死锁是指两个或多个线程在互斥地请求资源时发生的相互等待的情况,导致所有线程都无法继续执行。

//递归锁死锁的例子
- (void)recursiveDeadlocks {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            
            block = ^(int value) {
                [recursiveLock lock];
                if (value > 0) {
                    NSLog(@"value——%d %@", value, [NSThread currentThread]);
                    block(value - 1);
                }
                [recursiveLock unlock];
                NSLog(@"unlock—— %@", [NSThread currentThread]);
            };
            block(10);
        });
    }
}

请添加图片描述
从中我们可以看到每次打印的线程信息显示都不在同一个线程,这是因为我们的外侧for循环导致里面的异步操作几乎同时执行多个线程都执行了lock操作,最后导致单个线程的unlock操作无法解放完所有的lock,因为它们只能解放掉自己线程的lock,而找不到解放其他线程的锁的入口,于是最后就导致了线程的死锁

上述代码中展示了一种导致递归锁死锁的情况。在这段代码中,创建了一个 NSRecursiveLock 对象 recursiveLock,然后使用 GCD(Grand Central Dispatch)异步地在多个线程中执行递归调用。

在递归调用的内部,通过块(block)来实现递归,并在每次递归调用前后加锁和解锁 recursiveLock。然而,由于在异步线程中执行递归调用,多个线程可能会同时竞争 recursiveLock

当多个线程同时尝试获取 recursiveLock 时,由于递归锁的特性允许同一线程多次加锁,其中一个线程成功获取锁并进入递归调用,而其他线程在获取锁时会被阻塞等待。然而,由于 NSRecursiveLock 是可递归的,获取锁的线程在递归调用中再次尝试获取锁,这会导致它自身被阻塞等待自己释放锁,形成死锁。

死锁原因: 线程1加锁,同时线程2加锁 —> 解锁1等待解锁2—> 解锁2等待解锁1 —> 无法结束结束 —> 形成死锁。

解决

方案1:

去掉外侧的这个for循环的话就不会crash:因为代码调用的是 dispatch_async(dispatch_get_global_queue异步执行。我们的外侧for循环导致里面的异步操作几乎同时执行多个线程都执行了lock操作,最后导致单个线程的unlock操作无法解放完所有的lock,因为它们只能解放掉自己线程的lock,而找不到解放其他线程而找不到解放其他线程的锁的入口,于是最后就导致了线程的死锁。

前面提到过 同一线程可以多次获取而不会导致死锁的锁,重点是在同一线程。

没有外层for循环的那种情况不会crash的原因是十次lock和十次unlock操作都是在同一个线程中进行的,加锁和解锁的数量是相匹配的,最后刚好可以解放掉所有的锁使线程正常运行,所以它不会产生crash。

方案2: 解决这个问题的一种方法是使用信号量(Semaphore),确保同一线程在递归调用中只获取一次锁 代码如下:

- (void)OkRecursiveNoDeadlocks {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(concurrentQueue, ^{
        static void (^recursiveBlock)(int, dispatch_semaphore_t);

        recursiveBlock = ^(int value, dispatch_semaphore_t semaphore) {
            [recursiveLock lock];
            if (value > 0) {
                NSLog(@"value——%d %@", value, [NSThread currentThread]);
                recursiveBlock(value - 1, semaphore);
            }
            [recursiveLock unlock];
            NSLog(@"unlock—— %@", [NSThread currentThread]);

            dispatch_semaphore_signal(semaphore);
        };

        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        recursiveBlock(10, semaphore);
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//        dispatch_release(semaphore);
    });
}

请添加图片描述

能够看出来 就是在同一个线程完成操作,不会导致相互等待导致死锁。

NSRecursiveLock的实现

在介绍NSRecursiveLock的时候说了它是 NSLock 的子类,而NSLock又是基于pthread_mutex本身实现的,NSLockNSRecursiveLock的区别在于一个是非递归锁,一个是递归锁。

NSRecursiveLock如何成为递归锁,分析源码看看。

  • NSRecursiveLock
    在这里插入图片描述
    能看到 NSRecursiveLockNSLock的加锁方法是一样的。
  • 为什么NSRecursiveLock就可以支持可递归加锁呢?
    在这里插入图片描述

NSRecursiveLock方法支持可递归加锁,原因就是图中所示,在init方法中,通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))进行了递归的设置

pthread_mutexattr_settype 函数是用于设置互斥锁属性的函数,其中参数 attrspthread_mutexattr_t 类型的属性对象,用于存储互斥锁的属性信息。第二个参数是一个整型值,用于指定互斥锁的类型。

Int32(PTHREAD_MUTEX_RECURSIVE) 是将 PTHREAD_MUTEX_RECURSIVE 转换为 Int32 类型的整数值。PTHREAD_MUTEX_RECURSIVE 是一个宏定义,表示将互斥锁设置为递归锁的类型

NSCondition 【非递归互斥锁】

NSConditionFoundation 框架中的一个线程同步类,用于在多个线程之间进行条件变量的等待和通知。它提供了一种机制,允许一个线程在满足某个条件之前等待,而其他线程在满足条件时发出通知。

NSCondition是一个条件锁,同时其实也是一个非递归互斥锁,可能平时用的不多,但与GCD信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足。

NSCondition 的对象实际上作为⼀个锁和⼀个线程检查器。

  • 锁主要为了当检测条件时保护数据源,执⾏条件引发的任务;
  • 线程检查器主要是根据条件决定是否继续运⾏线程,即线程是否被阻塞。

使用:

  • [condition lock] :⼀般⽤于多线程同时访问、修改同⼀个数据源,保证在同⼀
    时间内数据源只被访问、修改⼀次,其他线程的命令需要在lock 外等待,只到
    unlock ,才可访问
  • [condition unlock]:与lock同时使⽤
  • [condition wait]:让当前线程处于等待状态
  • [condition signal]:CPU发信号告诉线程不⽤在等待,可以继续执⾏。

就用最基本的生产者消费者的例子举例。

当不加锁的时候

- (void)jp_testConditon {
    //创建生产-消费者
        for (int i = 0; i < 50; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_producer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_consumer];
            });
            
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_consumer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_producer];
            });
        }
    }

- (void)jp_producer{

        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_conditon signal];
    [_conditon unlock];
}
- (void)jp_consumer{
  
        if (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
         
        }
    // 注意消费行为 要在等待条件之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
   
    }

从运行结果,可以看到出现了负数的情况,我生产者生产的东西你都消费完了,已经没有了,你还在消费,就出现了线程不安全访问的事故了。

所以我们要保证生产线、消费线数据的安全,就需要进行加锁处理,以保证多线程安全,但这只是它们内部的得到保证了,但是它们之间存在消费关系,比如生产的库存没有了,就得通知消费者进行等待,生产好了再通知消费者来消费买单

请添加图片描述

  • 加锁
- (void)jp_testConditon {
    //创建生产-消费者
        for (int i = 0; i < 50; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_producer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_consumer];
            });
            
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_consumer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self jp_producer];
            });
        }
    }

- (void)jp_producer{
    [_conditon lock];
        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_conditon signal]; // 发送信号,通知消费者可以消费
    [_conditon unlock];
}
- (void)jp_consumer{
    [_conditon lock];
        if (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
            [_conditon wait]; // 等待生产者生产东西
        }
    // 注意消费行为 要在等待条件之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_conditon unlock];
    }

请添加图片描述

很明显,加锁之后的打印是正常的,没有出现负数,数据是安全的!

  • 如果产品不足就[_testCondition wait]进行等待,使得消费者停止消费
  • [_testCondition signal]模拟现在有生产了,可以来消费了,向等待的线程发送信号,通知来消费

NSCondition的实现

首先看swift发现NSCondition方法也是对pthread进行了封装,就是多了个对 cond的处理

请添加图片描述

_cond

_condNSCondition 类中的成员变量,它代表条件变量(condition variable)条件变量是一种线程同步机制,用于在线程之间进行等待和通知,以实现对特定条件的等待和唤醒操作。

  • NSCondition 的实现中,_cond 变量与互斥锁 _lock 配合使用。当一个线程需要等待某个条件时,它会先获取 _lock 互斥锁,然后调用 pthread_cond_wait 等待条件变量 _cond。此时,线程会被阻塞,并且会释放 _lock 互斥锁,允许其他线程继续执行。
  • 当满足条件的事件发生后,其他线程可以通过调用 pthread_cond_signalpthread_cond_broadcast 来发送信号或广播,唤醒等待的线程。被唤醒的线程会重新获取 _lock 互斥锁,并继续执行。
  • NSCondition的方法实现(swift)
    请添加图片描述
swift实现部分:
open class NSCondition: NSObject, NSLocking {
    internal var mutex = _MutexPointer.allocate(capacity: 1)
    internal var cond = _ConditionVariablePointer.allocate(capacity: 1)

    public override init() {
        pthread_mutex_init(mutex, nil)
        pthread_cond_init(cond, nil)
    }
    
    deinit {
        pthread_mutex_destroy(mutex)
        pthread_cond_destroy(cond)
        mutex.deinitialize(count: 1)
        cond.deinitialize(count: 1)
        mutex.deallocate()
        cond.deallocate()
    }
    
    // 一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,
    // 其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    // 释放锁,与lock成对出现
    open func unlock() {
        pthread_mutex_unlock(mutex)
    }
    
    // 让当前线程处于等待状态,阻塞
    open func wait() {
        pthread_cond_wait(cond, mutex)
    }

    // 让当前线程等待到某个时间,阻塞
    open func wait(until limit: Date) -> Bool {
        guard var timeout = timeSpecFrom(date: limit) else {
            return false
        }
        return pthread_cond_timedwait(cond, mutex, &timeout) == 0
    }
    
    // 发信号告诉线程可以继续执行,唤醒线程
    // 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
    open func signal() {
        pthread_cond_signal(cond)
    }
    
    //唤醒所有正在等待的线程
    open func broadcast() {
        pthread_cond_broadcast(cond) // wait  signal
    }
    
    open var name: String?
}

  • (void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
  • (void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
  • (void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
  • (void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。

NSLock NSRecursiveLock NSCondition总结

  • NSLock不支持递归加锁
  • NSRecursiveLock虽然有递归性,但没有多线程特性
  • NSCondition 的对象实际上作为⼀个锁和⼀个线程检查器

NSConditionLock【非递归互斥锁】

NSConditionLock是对NSCondition的二次封装,他也是非递归互斥锁。

NSConditionLock自带条件探测,只需要我们传入一个值就可==即可知道制定的线程锁执行任务。

- (void)testNSConditionLock {
    /*dispatch_get_global_queue 函数是用于获取全局并发队列的函数,它接受两个参数:优先级和标志。参数 0 表示默认优先级,而第二个参数 0 表示没有特定的标志。*/
    /*所以线程的外部异步调用顺序是 线程1 线程3 线程2*/
    /*但是线程1不匹配 进入线程3 此时当前的线程3调用[NSConditionLock lock:],本质上是调用 [NSConditionLock lockBeforeDate:],这里不需要比对条件值,所以线程3会打印*/
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
           [conditionLock lockWhenCondition:1];
           NSLog(@"线程1");
           [conditionLock unlockWithCondition:0];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
           [conditionLock lockWhenCondition:2];
           NSLog(@"线程2");
           [conditionLock unlockWithCondition:1];
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           [conditionLock lock];
           NSLog(@"线程3");
           [conditionLock unlock];
        });
}

请添加图片描述
解释一下:

  • *dispatch_get_global_queue 函数是用于获取全局并发队列的函数,它接受两个参数:优先级和标志。参数 0 表示默认优先级,而第二个参数 0 表示没有特定的标志。
    请添加图片描述
  • 所以线程的外部异步调用顺序是 线程1 线程3 线程2
  • 但是线程1不匹配 进入线程3 此时当前的线程3调用[NSConditionLock lock:],本质上是调用 [NSConditionLock lockBeforeDate:],这里不需要比对条件值,所以线程3会打印
  • 接下来线程2执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线程2会打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将条件设置为 1,并发送boradcast, 此时线程1接收到当前的信号,唤醒执行并打印。
    自此当前打印为 线程 3->线程 2 -> 线程 1。
  • [NSConditionLock lockWhenCondition:]:这里会根据传入的condition 值和value值进行对比,如果不相等,这里就会阻塞。
  • [NSConditionLock unlockWithCondition:]会先更改当前的value值,然后调用boradcast,唤醒当前的线程。
      • broadcastNSCondition 类中的一个方法,用于向所有等待的线程发送广播通知,以唤醒它们继续执行。
        NSCondition 中,broadcast 方法的作用是发送广播信号给所有正在等待的线程,通知它们条件已经满足,可以继续执行。与 signal 方法不同的是,broadcast 方法会同时唤醒多个等待的线程,而不是仅唤醒一个线程。

NSCondition和NSConditionLock总结

相同点:

  • 都是互斥非递归锁
  • 过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的

不同点:

  • NSCondition是基于对pthread_mutex的封装,而NSConditionLock是对NSCondition做了一层封装
  • NSCondition是需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程
  • NSConditionLock则只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

NSConditionLock实现

下面是对给出的 NSConditionLock 源码进行注释说明:

internal var _cond = NSCondition()  // 创建 NSCondition 对象,用于条件变量的同步操作
internal var _value: Int  // 记录条件变量的值
internal var _thread: _swift_CFThreadRef?  // 记录当前持有锁的线程

public convenience override init() {
    self.init(condition: 0)
}

public init(condition: Int) {
    _value = condition
}

open func lock() {
    _cond.lock()  // 获取互斥锁

    while _thread != nil {  // 如果已经有线程持有锁,进入等待状态
        _cond.wait()
    }

    _thread = _swift_CFThreadRefGetCurrent()  // 记录当前持有锁的线程
    _cond.unlock()  // 释放互斥锁
}

open func unlock() {
    _cond.lock()  // 获取互斥锁

    _thread = nil  // 清空当前持有锁的线程
    _cond.broadcast()  // 广播通知所有等待的线程
    _cond.unlock()  // 释放互斥锁
}

open var condition: Int {
    return _value
}

open func lock(whenCondition condition: Int) {
    _cond.lock()  // 获取互斥锁

    while _thread != nil || _value != condition {  // 如果已经有线程持有锁,或者条件变量的值不满足指定条件,进入等待状态
        _cond.wait()
    }

    _thread = _swift_CFThreadRefGetCurrent()  // 记录当前持有锁的线程
    _cond.unlock()  // 释放互斥锁
}

open func `try`() -> Bool {
    return lock(before: Date.distantPast)  // 尝试获取锁,一直等待直到过去的时间
}

open func tryLock(whenCondition condition: Int) -> Bool {
    return lock(whenCondition: condition, before: Date.distantPast)  // 尝试获取锁,并且条件变量的值满足指定条件,一直等待直到过去的时间
}

open func unlock(withCondition condition: Int) {
    _cond.lock()  // 获取互斥锁

    _thread = nil  // 清空当前持有锁的线程
    _value = condition  // 更新条件变量的值
    _cond.broadcast()  // 广播通知所有等待的线程
    _cond.unlock()  // 释放互斥锁
}

open func lock(before limit: Date) -> Bool {
    _cond.lock()  // 获取互斥锁

    while _thread != nil {  // 如果已经有线程持有锁,进入等待状态
        if !_cond.wait(until: limit) {  // 等待直到指定的时间限制
            _cond.unlock()  // 在等待过程中超时,释放互斥锁并返回失败
            return false
        }
    }

    _thread = _swift_CFThreadRefGetCurrent()  // 记录当前持有锁的线程


    _cond.unlock()  // 释放互斥锁
    return true
}

open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
    _cond.lock()  // 获取互斥锁

    while _thread != nil || _value != condition {  // 如果已经有线程持有锁,或者条件变量的值不满足指定条件,进入等待状态
        if !_cond.wait(until: limit) {  // 等待直到指定的时间限制
            _cond.unlock()  // 在等待过程中超时,释放互斥锁并返回失败
            return false
        }
    }

    _thread = _swift_CFThreadRefGetCurrent()  // 记录当前持有锁的线程
    _cond.unlock()  // 释放互斥锁
    return true
}

open var name: String?  // 条件锁的名称,可选属性
  • NSConditionLock 类使用了一个 NSCondition 对象 _cond 来进行线程同步和条件变量的控制。
  • _value 变量记录了条件变量的值,而 _thread 变量则记录了当前持有锁的线程。
  • lock() 方法用于获取锁,如果已经有线程持有锁,则当前线程进入等待状态。unlock() 方法释放锁,并通知所有等待的线程。
  • lock(whenCondition:) 方法和 lock(before:) 方法在获取锁的基础上增加了条件判断和超时控制。
  • try() 方法和 tryLock(whenCondition:) 方法用于尝试获取锁,并返回是否成功。
  • unlock(withCondition:) 方法在释放锁的同时,更新条件变量的值,并通知所有等待的线程。
  • name 属性用于设置条件锁的名称。

Semaphore

GCD 中的信号量是指 Dispatch Semaphore:持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,可通过。

根据定义它也是互斥锁。

使用时机

  • 信号量的使用时机是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量
    Dispatch Semaphore 在实际开发中主要用于:
  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁
    请添加图片描述

总结

相比于锁的实现,锁的用法更好理解,需要在合适的时候调用合适的锁,还有一个@synchronized还没有学习,这个锁是比较重要的,下一篇会详细学习@synchronized的用法和实现过程。

本次学习理解了互斥锁和自旋锁的原理和区别。 还了解到各种递归锁和非递归锁的用法和原理,对于NSLock 就是基于互斥锁本身实现的一个锁,以及NSCondition NSConditionLock等都会涉及到pthread_mutex,所以 pthread_mutex还是基础并且也非常的重要‼️。

在学习和了解锁的使用过程用到了很多GCD的知识,一般都是基于async并发执行的过程需要加锁,也是一种复习。

接下来会学习在各种源码常见的非递归互斥锁 @synchronized

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。