iOS中的锁

参考的博客:

iOS八股文(十四)iOS中的锁
[iOS开发]iOS中相关锁
iOS进阶-细数iOS中的锁
iOS探索 细数iOS中的那些锁

线程安全

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。 在iOS中,UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题

iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。

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

下方我们就详细讲解iOS相关锁,本博客采用一个经典的售票例子:

此处展示的是不加锁(即不考虑线程安全)的情况:

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

@end

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

- (void) startSell {
	//一共有50张票
	self.ticketSurplusCount = 50;
	   
	__weak typeof (self) weakSelf = self;
	    
	//一号售票窗口    
	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];
	    }
	});
}

//售票的方法
- (void)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); 
            break;
        }
    }
}

运行结果如下:

请添加图片描述
我们看到运行结果显示整个卖票的过程是错乱的,居然一共卖出了52张票,接下来我们就在讲解锁的过程中对卖票操作加锁,来修正现在的错乱结果。

锁的种类

iOS中的锁有两大类:自旋锁互斥锁

自旋锁:

与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环尝试,直到该自旋锁的保持者已经释放了锁(忙等待)因为不会引起调用者睡眠,所以效率高于互斥锁

自旋锁原理:

线程一直是running(加锁——>解锁)死循环检测锁的标志位,机制不复杂。

自旋锁缺点:

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

OSSpinLock(自旋锁)

OSSpinLock是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>,使用时会出现警告⚠️

请添加图片描述
这是因为OSSpinLock存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock来替代。

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

实际使用(在卖票例子中):

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

运行效果:
请添加图片描述
结果就是按照顺序非常规范地卖出了这50张票

刚才提到了OSSpinLock存在缺陷,其实它的缺陷主要存在两点:

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

值得注意的是: 自旋锁和优先级反转没有关系,但是正因为有上面两点,所以自旋锁会导致优先级反转问题更难解决,甚至造成更为严重的线程等待问题,所以苹果就废除了OSSpinLock,转而推荐人们使用os_unfair_lock来替代,由于os_unfair_lock是一个互斥锁,所以我们将对其的讲解放到互斥锁中去。

互斥锁:

保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒

互斥锁原理:

线程会从sleep(加锁——> running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销,所以效率是要低于自旋锁的。

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

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

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

os_unfair_lock

上面讲过现在苹果采用os_unfair_lock来代替不安全的OSSpinLock,且由于os_unfair_lock会休眠而不是忙等,所以属于 互斥锁 ,且是非递归互斥锁,下面来看一下它的用法:

os_unfair_lockos库中,使用之前需要导入头文件<os/lock.h>

//创建一个锁
os_unfair_lock_t unfairLock;
//初始化
unfairLock = &(OS_UNFAIR_LOCK_INIT);
//加锁
os_unfair_lock_lock(unfairLock);
//解锁
os_unfair_lock_unlock(unfairLock);

实际使用(在卖票例子中):

//售票的方法
- (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);
    }
}

运行结果如下:
请添加图片描述
结果就是按照顺序非常规范地卖出了这50张票

其实关于这个不公平锁的分类,网上产生了很多分歧,经过我们查阅官方文档得到:
请添加图片描述
请添加图片描述
其中第二张图的翻译如下:
请添加图片描述
可以看到这里的解释是,不是旋转(忙等)而是休眠等待被唤醒,所以os_unfair_lock理应是互斥锁。

pthread_mutex

pthread_mutex就是 互斥锁 本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠,另外pthread_mutex也是非递归的锁

使用时我们需要先引用这个头文件:#import <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_t _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);
    }
}

运行结果如下:
请添加图片描述
结果就是按照顺序非常规范地卖出了这50张票

NSLock

我们的Foundation框架内部也是有一把NSLock锁的,使用起来非常方便,基于互斥锁pthroad_mutex封装而来,是一把互斥非递归锁
使用如下:

//初始化NSLock
NSLock *lock = [[NSLock alloc] init];
//加锁
[lock lock];
...
//线程安全执行的代码
...
//解锁
[lock unlock];

实际使用(在卖票例子中):

//售票的方法
- (void)saleTicketSafe {
    while (1) {
        // 加锁
        [_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);
            // 解锁
            [_lock unlock];
            break;
        }
        // 解锁
        [_lock unlock];
    }
}

运行结果如下:
请添加图片描述
结果就是按照顺序非常规范地卖出了这50张票

如果对非递归锁强行使用递归调用,就会在调用时发生线程阻塞,而并非是死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。

苹果官方文档的描述如下:
请添加图片描述
翻译版如下:
请添加图片描述
可以看到在同一线程上调用两次NSLocklock方法将会永久锁定线程。同时也重点提醒向NSLock对象发生解锁消息时,必须确保消息时从发送初始锁定消息的同一个线程发送的,否则就会产生未知问题。

我们在这里举一个非递归互斥锁导致线程阻塞的例子:

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@property (nonatomic, strong) NSLock *lock;

@end


//卖票窗口(此处我们循环创建十个窗口,但是都是在同一线程中执行))
- (void)threadBlock {   
	//一共50张票
	self.ticketSurplusCount = 50;
	//初始化NSLock非递归锁
	_lock = [[NSLock alloc] init];
	
    __weak typeof (self) weakSelf = self;

    for (int i = 0; i < 10; ++i) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [weakSelf saleTicket];
        });
    }
}

//卖票的函数
- (void)saleTicket {
    //加锁
	[_lock lock];
    if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
        self.ticketSurplusCount--;
        cnt++;
        NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
        [NSThread sleepForTimeInterval:0.2];
        //递归调用卖票函数
        [self saleTicket];
    } else { // 如果已卖完,关闭售票窗口
        NSLog(@"所有火车票均已售完,共售出%d张票", cnt);    
    }
    //解锁
    [_lock unlock];
}

运行结果如下:
请添加图片描述
可以看到,因为我们对当前这个线程在执行lock操作后还未unlock的情况下,又进行了NSLock的重复lock加锁操作,所以当前线程发生了阻塞,只进行了一次卖票操作就再不执行其他操作了。

NSRecusiveLock

NSRecursiveLock使用和NSLock类似,不过NSRecursiveLock递归互斥锁

//初始化NSLock
NSRecusiveLock *recusiveLock = [[NSRecusiveLock alloc] init];
//加锁
[recusiveLock lock];
...
//线程安全执行的代码
...
//解锁
[recusiveLock unlock];

下面我们举一个NSRecursiveLock递归使用的例子:

@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;

@end


//卖票窗口(此处我们循环创建十个窗口,但是都是在同一线程中执行)
- (void)threadBlock {   
	//一共50张票
	self.ticketSurplusCount = 50;
	//初始化NSRecursiveLock递归锁
    _recursiveLock = [[NSRecursiveLock alloc] init];
	
    __weak typeof (self) weakSelf = self;

    for (int i = 0; i < 10; ++i) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [weakSelf saleTicket];
        });
    }
}

//卖票的函数
- (void)saleTicket {
    //加锁
    [_recursiveLock lock];
    if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
        self.ticketSurplusCount--;
        cnt++;
        NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
        [NSThread sleepForTimeInterval:0.2];
        //递归调用卖票函数
        [self saleTicket];
    } else { // 如果已卖完,关闭售票窗口
        NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
    }
    //解锁
    [_recursiveLock unlock];
}

运行结果如下:
请添加图片描述
可以看到向同一个线程多次获取递归锁NSRecusiveLock并不会导致程序死锁,而是正常的线程安全地加锁执行。

苹果官方文档的描述如下:
请添加图片描述
翻译为中文为:
请添加图片描述
同一线程可以多次获取而不会导致死锁的锁,重点是在同一线程

但是我们上边说了: 对于递归锁我们要注意使用时死锁问题,前后代码相互等待就会死锁
下面我们就举一个递归锁死锁的例子:

//递归锁死锁的例子
- (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);
        });
    }
}

运行结果如下:
请添加图片描述
我们可以看到程序出现了crash
但是我们去掉外侧的这个for循环的话就不会crash

- (void)recursiveDeadlocks {
    NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
    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);
    });
}

原因是什么呢,我们在lock操作前再加一个打印线程信息的操作就能看出些端倪了:

//递归锁死锁的例子
- (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) {
            	//新添加的打印线程信息的操作
            	NSLog(@"lock——%@", [NSThread currentThread]);
                [recursiveLock lock];
                if (value > 0) {
                    NSLog(@"value——%d %@", value, [NSThread currentThread]);
                    block(value - 1);
                }
                [recursiveLock unlock];
                NSLog(@"unlock—— %@", [NSThread currentThread]);
            };
            block(10);
        });
    }
}

运行结果如下:
请添加图片描述
可以看到是crash了,而且crash的地方都和刚才不一样,应该是由于新加的这个打印操作对程序的执行时间进行了拖延才导致的提前crash,但是我们主要关注的是打印的lock操作前的线程信息,从中我们可以看到每次打印的线程信息显示都不在同一个线程,这是因为我们的外侧for循环导致里面的异步操作几乎同时执行多个线程都执行了lock操作,最后导致单个线程的unlock操作无法解放完所有的lock,因为它们只能解放掉自己线程的lock,而找不到解放其他线程的锁的入口,于是最后就导致了线程的死锁。但是我们没有外层for循环的那种情况不会crash的原因是十次lock和十次unlock操作都是在同一个线程中进行的,加锁和解锁的数量是相匹配的,最后刚好可以解放掉所有的锁使线程正常运行,所以它不会产生crash

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

NSCondition

NSCondition是一个条件锁,同时其实也是一个非递归互斥锁,可能平时用的不多,但与GCD信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足,一旦获得了锁并执行了代码的关键部分,线程就可以放弃该锁并将关联条件设置为新的条件。条件本身是任意的:可以根据应用程序的需要定义它们。

Objective-C代码并不能看到NSCondition的具体实现,只能看到该类的接口部分,实现部分需要使用swift源码进行查看:

OC接口部分:
@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end
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?
}

可以看到,该对象还是对pthread_mutex的一层封装,NSCondition也是一种互斥锁。当我们需要等待某个条件的时候,也就是条件不满足的时候,就可以使用wait方法来阻塞线程,当条件满足了,使用signal方法发送信号唤醒线程。

再浅浅总结一下:

  • NSCondition是对mutexcond的一种封装(cond就是用于访问和操作特定类型数据的指针)
  • wait操作会阻塞线程,使其进入休眠状态,直至超时
  • signal操作是唤醒一个正在休眠等待的线程
  • broadcast会唤醒所有正在等待的线程

实际使用(在卖票例子中):

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

运行结果如下:
请添加图片描述
结果就是按照顺序非常规范地卖出了这50张票

NSConditionLock

NSConditionLockNSCondition又做了一层封装,自带条件探测,能够更简单灵活的使用,所以它也属于非递归互斥锁
然后我们来看一看NSConditionLock的相关源码:

NSCondition的源码一样,在OC中看接口,在swift中看实现:

OC的接口部分:
@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end
swift中实现:
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
    
public convenience override init() {
    self.init(condition: 0)
}
    
public init(condition: Int) {
    _value = condition
}

// 表示 xxx 期待获得锁,
// 如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,
// 如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
open func lock() {
    let _ = lock(before: Date.distantFuture)
}

open func unlock() {
    _cond.lock()
    _thread = nil
    _cond.broadcast()
    _cond.unlock()
}
    
open var condition: Int {
    return _value
}

// 表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。
// 如果内部的condition等于A条件,并且没有其他线程获得该锁,则执行任务,同时设置它获得该锁
// 其他任何线程都将等待它代码的完成,直至它解锁。
open func lock(whenCondition condition: Int) {
    let _ = lock(whenCondition: condition, before: Date.distantFuture)
}

open func `try`() -> Bool {
    return lock(before: Date.distantPast)
}
    
open func tryLock(whenCondition condition: Int) -> Bool {
    return lock(whenCondition: condition, before: Date.distantPast)
}

// 表示释放锁,同时把内部的condition设置为A条件
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 = pthread_self()
    _cond.unlock()
    return true
}
    
// 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。
// 需要注意的是:返回的值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理
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 = pthread_self()
    _cond.unlock()
    return true
}
    
open var name: String?

可以看出,触发的唤醒线程的条件是传入的condition取值,和我们创建锁的时候值要相同,我们可以在释放当前线程锁的时候重新设置其他线程传入的condition值,这样也就达到了唤醒其他线程的目的。如果创建锁的值和传入的值都不能匹配,则会进入阻塞状态

也就是说NSConditionLockinitlockunlock中都可以传入value

例如:

- (void)conditionLockTest {
    for (int i = 0; i < 5; ++i) {
        //调用测试函数
        [self test];
        //修改Condition参数值为3
        [self.conditionLock lockWhenCondition:0];
        [self.conditionLock unlockWithCondition:3];
    }
    return;
}

//测试函数
- (void)test {
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:3];
    dispatch_queue_t globalQ = dispatch_get_global_queue(0, 0);
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:3];
        NSLog(@"任务1");
        [self.conditionLock unlockWithCondition:2];
    });
    
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"任务2");
        [self.conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:1];
        NSLog(@"任务3");
        [self.conditionLock unlockWithCondition:0];
    });
}

运行结果如下:
请添加图片描述
我们看到每次打印的结果都是严格按照任务1、任务2、任务3执行的。

总结NSConditionLockNSCondition

  1. 相同点:
    • 都是互斥锁
    • 通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的
  2. 不同点:
    • NSCondition是基于对pthread_mutex的封装,而NSConditionLock是对NSCondition做了一层封装
    • NSCondition是需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock则只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

Semaphore信号量

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

Dispatch Semaphore 提供了三个方法:

dispatch_semaphore_create://创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal://发送一个信号,让信号总量加 1
dispatch_semaphore_wait://可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意: 信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

@synchronized

@synchronized可能是日常开发中用的比较多的一种递归互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低

使用方法如下:

@synchronized (obj) {}

下面我们来探索一下@synchronized的源码:

  • 通过汇编能发现@synchronized就是实现了objc_sync_enterobjc_sync_exit两个方法请添加图片描述
  • 通过符号断点能知道这两个方法都是在objc源码中的
  • 通过clang也能得到一些信息:
#pragma clang assume_nonnull end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        { id _rethrow = 0; id _sync_obj = (id)__null; objc_sync_enter(_sync_obj);
try {
	struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
	~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
	id sync_exit;
	} _sync_exit(_sync_obj);

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_main_59328a_mi_0);
        } catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
	~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
	id rethrow;
	} _fin_force_rethow(_rethrow);}
}

    }
    return 0;
}
objc源码分析

objc源码中找到objc_sync_enterobjc_sync_exit

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
	

    return result;
}
  1. 首先从它的注释中recursive mutex可以得出@synchronized是递归锁
  2. 如果锁的对象obj不存在时分别会走objc_sync_nil()和不做任何操作(源码分析可以先解决简单的逻辑分支)
BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

这也是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁

  1. 正常情况下(obj存在)会通过id2data方法生成一个SyncData对象
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;
  • nextData指的是链表中下一个SyncData
  • object指的是当前加锁的对象
  • threadCount表示使用该对象进行加锁的线程数
  • mutex即对象所关联的锁
准备SyncData
static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    后方代码省略...
}

id2data先将要返回的SyncData类型的对象result初始化好,后续进行数据填充

//使用多个并行列表来减少不相关对象之间的争用。
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

其中通过两个宏定义去取得SyncList中的datalock——static StripedMap<SyncList> sDataLists 可以理解成 NSArray<id> list

既然@synchronized能在任意地方(VC、View、Model等)使用,那么底层必然维护着一张全局的表(类似于weak表)。而从SyncListSyncData的结构可以证实系统确实在底层维护着一张哈希表,里面存储着SyncList结构的数据。SyncListSyncData的关系如下图所示:
在这里插入图片描述

使用快速缓存
static SyncData* id2data(id object, enum usage why)
{
    ...
#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    // 检查每线程单项快速缓存中是否有匹配的对象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif
    ...
}

这里有个重要的知识点——TLSTLS全称为Thread Local Storage,在iOS中每个线程都拥有自己的TLS,负责保存本线程的一些变量, 且TLS无需锁保护

快速缓存的含义为:定义两个变量SYNC_DATA_DIRECT_KEY/SYNC_COUNT_DIRECT_KEY,与tsl_get_direct/tls_set_direct配合可以从线程局部缓存中快速取得SyncCacheItem.dataSyncCacheItem.lockCount

如果在缓存中找到当前对象,就拿出当前被锁的次数lockCount,再根据传入参数类型(获取、释放、查看)对lockCount分别进行操作

  • 获取资源ACQUIRElockCount++并根据key值存入被锁次数
  • 释放资源RELEASElockCount--并根据key值存入被锁次数。如果次数变为0,此时锁也不复存在,需要从快速缓存移除并清空线程数threadCount
  • 查看资源check:不操作

lockCount表示被锁的次数,意味着能多次进入,从侧面表现出了递归性

获取该线程下的SyncCache

这个逻辑分支是找不到确切的线程标记时只能进行所有的缓存遍历

static SyncData* id2data(id object, enum usage why)
{
    ...
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
    ...
}

这里介绍一下SyncCacheSyncCacheItem

typedef struct {
    SyncData *data;             //该缓存条目对应的SyncData
    unsigned int lockCount;     //该对象在该线程中被加锁的次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;     //该缓存此时对应的缓存大小
    unsigned int used;          //该缓存此时对应的已使用缓存大小
    SyncCacheItem list[0];      //SyncCacheItem数组
} SyncCache;
  • SyncCacheItem用来记录某个SyncData在某个线程中被加锁的记录,一个SyncData可以被多个SyncCacheItem持有
  • SyncCache用来记录某个线程中所有SyncCacheItem,并且记录了缓存大小以及已使用缓存大小
全局哈希表查找

快速、慢速流程都没找到缓存就会来到这步——在系统保存的哈希表进行链式查找

static SyncData* id2data(id object, enum usage why)
{
    ...
    lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }
    ...
}
  1. lockp->lock()并不是在底层对锁进行了封装,而是在查找过程前后进行了加锁操作
  2. for循环遍历链表,如果有符合的就goto done
    寻找链表中未使用的SyncData并作标记
  3. 如果是RELEASECHECK直接goto done
  4. 如果第二步中有发现第一次使用的的对象就将threadCount标记为1goto done
生成新数据并写入缓存
static SyncData* id2data(id object, enum usage why)
{
    ...
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }
    ...
}
  1. 第三步情况均不满足(即链表不存在——对象对于全部线程来说是第一次加锁)就会创建SyncData并存在result里,方便下次进行存储
  2. done分析:
    • 先将前面的lock锁解开
    • 如果是RELEASE类型直接返回nil
    • ACQUIRE类型和对象的断言判断
    • !fastCacheOccupied分支表示支持快速缓存且快速缓存没有被占用,将该SyncCacheItem数据写入快速缓存中
    • 否则将该SyncCacheItem存入该线程对应的SyncCache
一些问题
  1. 不能使用非OC对象作为加锁条件——id2data中接收参数为id类型
  2. 多次锁同一个对象会有什么后果吗——会从高速缓存中拿到data,所以只会锁一次对象
  3. 都说@synchronized性能低——是因为在底层增删改查消耗了大量性能
  4. 加锁对象不能为nil,否则加锁无效,不能保证线程安全

举个例子:

- (void)test {
    _testArray = [NSMutableArray array];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self.testArray) {
                self.testArray = [NSMutableArray array];
            }
        });
    }
}

实测运行结果:
请添加图片描述

上面代码一运行就会崩溃,原因是因为在某一瞬间testArray释放了为nil,导致哈希表中存的对象也变成了nil,导致synchronized无效化

解决方案:

  • self进行同步锁,但这个似乎太臃肿了
  • 使用NSLock
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值