ios 线程休眠_iOS底层分析 - 线程锁(一)NSLock

大纲

常用锁介绍

自旋锁和互斥锁的一些问题

NSLock及源码分析

NSLock 坑

一、常用锁介绍

锁的目的是为了解决资源抢夺

iOS中的常用的锁有如下几种:

1、自旋锁:

使用与多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直到显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。NSSpinLock ,它现在被废弃了,不能使用了,它是有缺陷的,会造成死锁

2、互斥锁

是一种用于多线程编程中,防止两条线程同时对同一公共资源(例如:同一个全局变量)进行读写的机制。互相排斥。例如线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:递归锁和非递归锁。互斥锁是通过将代码切片成一个一个的临时区来实现。p_thread_mutex,NSLock,@synchronized这个顺序是按照性能排序的,也是我们常用的几个互斥锁。

3、读写锁:

计算机程序的并发控制的一种同步机制,也称“共享 - 互斥锁”、多个读者,单个作者(写入)的锁机制。用于解决多线程对公共资源读写问题,读操作可并发重入,写操作时互斥的。读写锁通常用互斥锁、条件变量、信号量实现。

4、信号量:

是一种更高级的同步机制,有更多的取值空间。用来实现更加复杂的同步,而不单单是线程间互斥。semphone在一定程度也可以当互斥锁用,它适用于编程逻辑更复杂的场景,同时它也是除了自旋锁以外性能最高的锁

5、条件锁:

就是条件变量,当进程的某些资源要求不满足时就锁住进入休眠。当资源被分配到了,条件锁打开继续运行。NSCondition,条件锁我们调用wait方法就把当前线程进入等待状态,当调用了signal方法就可以让该线程继续执行,也可以调用broadcast广播方法。

临时区:

指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

锁是线程编程同步工具的基础。iOS开发中常用的锁有如下几种:

@synchronized

NSLock 对象锁

NSRecursiveLock 递归锁

NSConditionLock 条件锁

pthread_mutex 互斥锁(C语言)

dispatch_semaphore 信号量实现加锁(GCD)

OSSpinLock (暂不建议使用,原因参见这里)

二、自旋锁和互斥锁的一些问题

互斥锁

互斥锁又分 递归锁(NSRecursiveLock 等) 和 非递归锁(NSLock 等)。

递归锁:可重入锁,统一线程在锁释放前可再次获取锁,即可以递归调用

非递归锁:不可重入,必须等锁释放后才能再次获取锁。

自旋锁和互斥锁的区别?

互斥锁:当线程获取锁但没有获取到时,线程进入休眠状态。等到锁被释放,线程会被唤醒同时获取到锁。继续执行任务改变线程状态。

自旋锁:当线程获取锁没有获取到时,不会进入休眠,而是一直循环看是否可用。线程一直处于活跃状态,不会改变线程状态。

自旋锁和互斥锁的使用场景分别是?

自旋锁:由于自旋锁一直等待会消耗较多CPU 资源,但是效率较高一旦锁释放立刻就能执行无序唤醒。所以适用于短时间内的轻量级锁定。

互斥锁:需要修改线程状态,唤醒或休眠线程。所以适用于时间长相对自旋锁效率低的场景。

四、NSLock及源码分析

NSLock

非递归 互斥锁。NSLock 互斥锁 不能多次调用 lock方法,会造成死锁

遵循 NSLocking 协议。进行加锁、解锁

@protocol NSLocking

(void)lock;//加锁

(void)unlock;//解锁

@end

NSLock实现了NSLocking协议:

@interface NSLock : NSObject {

@private

void *_priv;

}

// 尝试获取锁,获取到返回YES,获取不到返回NO

(BOOL)tryLock;

// 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO

(BOOL)lockBeforeDate:(NSDate *)limit;

// 锁名称,如果使用锁出现异常,输出的log中会有

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

@end

-tryLock:如果能获取到锁返回YES,如果获取不到锁返回NO,但不会使线程进入休眠,会继续向下执行

-lockBeforeDate::如果锁已被锁定,在指定的时间点之前线程进入休眠等待锁释放。如果在时间点之前锁被释放了,线程立即被唤醒获得锁,该函数会返回YES,继续执行任务,不会一直休眠等到那个时间点。如果等到时间点还没有获得锁会返回NO,并继续执行任务

NSLock是非递归锁,不能重入,否则会发生死锁:

import "LJLNSLockViewController.h"

@interface LJLNSLockViewController ()

@property(nonatomic, strong) NSLock *lock;

@end

@implementation LJLNSLockViewController

(void)viewDidLoad {

[super viewDidLoad];

self.lock = [[NSLock alloc] init];

[NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];

}

//2020-04-06 13:49:43.713653+0800 filedome[74121:2725361] testLock1

(void)testLock1 {

[self.lock lock];

NSLog(@"testLock1");

[self testLock2];

[self.lock unlock];

NSLog(@"testLock1: unlock");

}

(void)testLock2 {

[self.lock lock];

NSLog(@"testLock2");

[self.lock unlock];

NSLog(@"testLock2: unlock");

}

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

注意:

-lock和-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用,否则会产生未知结果。

官方文档原文:Unlocking a lock from a different thread can result in undefined behavior.

在不断循环递归,多线程操作的时候。这个时候 _testArray 不断的去初始化新增,release 旧值 如果多条线程访同时敢问到release 那么就会造成多次释放这时候就需要禁止重入的互斥锁。例如:

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

for (int i=0; i<200000; i++) {

dispatch_async(dispatch_get_global_queue(0, 0), ^{

[lock lock];

_testArray = [NSMutableArray array];

[lock unlock];

});

}

NSLock 源码

NSLock是在Foundation中实现的,开源的Foundation是Swift版的:

源码 目录:demos->003-锁分析->2-Foundation源码

open func lock() {

pthread_mutex_lock(mutex)

}

open func unlock() {

pthread_mutex_unlock(mutex)

if os(macOS) || os(iOS)

// Wakeup any threads waiting in lock(before:)

pthread_mutex_lock(timeoutMutex)

pthread_cond_broadcast(timeoutCond)

pthread_mutex_unlock(timeoutMutex)

endif

}

// 对应OC中的 -tryLock

open func `try`() -> Bool {

return pthread_mutex_trylock(mutex) == 0

}

// 对应OC中的 -lockBeforeDate:

open func lock(before limit: Date) -> Bool {

if pthread_mutex_trylock(mutex) == 0 {

return true

}

return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)

}

private func timedLock(mutex: _MutexPointer, endTime: Date,

using timeoutCond: _ConditionVariablePointer,

with timeoutMutex: _MutexPointer) -> Bool {

var timeSpec = timeSpecFrom(date: endTime)

while var ts = timeSpec {

let lockval = pthread_mutex_lock(timeoutMutex)

precondition(lockval == 0)

let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)

precondition(waitval == 0 || waitval == ETIMEDOUT)

let unlockval = pthread_mutex_unlock(timeoutMutex)

precondition(unlockval == 0)

if waitval == ETIMEDOUT {

return false

}

let tryval = pthread_mutex_trylock(mutex)

precondition(tryval == 0 || tryval == EBUSY)

if tryval == 0 { // The lock was obtained.

return true

}

// pthread_cond_timedwait didn't timeout so wait some more.

timeSpec = timeSpecFrom(date: endTime)

}

return false

}

通过源码可知验证 NSLock 是对 pthread 中互斥锁 的封装。

其他都好理解,这里列一下 timedLock() 的实现流程:

1、设定超时时间,进入while循环。

2、pthread_cond_timedwait()在本次循环中计时等待,线程进入休眠

3、等待超时,直接返回 false;

4、如果等待没有超时,期间锁被释放,线程会被唤醒,再次尝试获取锁 pthread_mutex_trylock(),如果获取成功返回true

5、即没有超时,被唤醒后也没有成功获取到锁(被其他线程抢先获得锁),重新计算超时时间进入下一次while循环

五、NSLock 坑

如下代码。递归调用 testMethod (方法)输出,正常应该是10 9 8 .....,但是实际只输出了10.

原因分析:因为在if 之前进行锁了之后,在if 里面有递归调用了 testMethod 方法,又一次进来有锁了,这样被锁了多次都没去执行解锁一直处理阻塞。

这里应该换成递归锁.

例如:NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];

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

dispatch_async(dispatch_get_global_queue(0, 0), ^{

static void(^testMethod)(int);

testMethod = ^(int value){

[lock lock];

if (value >0) {

NSLog(@"current value = %d",value);

testMethod(value-1);

}

[lock unlock];

};

testMethod(10);

});

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值