iOS线程锁探究

多线程在我们开发中,被广泛应用,让应用程序的性能得到了很大的提高,但是在一些应用场景却会问题。比如一个电影院中有9张电影票,开设了三个售票窗口,他们同时开始售票,代码如下:

#import "ViewController.h"
CGFloat totalTicket = 9;
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i=0; i<totalTicket; i++) {
            [self saleTicketsMether];
        }
    });
}

-(void)saleTicketsMether{
    if (totalTicket>0) {
        sleep(0.1);
        totalTicket--;
        NSLog(@"totalTicket:%f",totalTicket);
    }
}

打印输出结果:

2018-10-30 12:01:55.049887 + 0800线程锁-18-10-30-0 [2783:84995] totalTicket:7.000000

2018-10-30 12:01:55.049887 + 0800线程锁-18-10-30-0 [2783:84998] totalTicket:8.000000

2018-10-30 12:01:55.049888 + 0800线程锁-18-10-30-0 [2783:84997] totalTicket:6.000000

2018-10-30 12:01:55.050008 + 0800线程锁-18-10-30-0 [2783:84995] totalTicket:3.000000

2018-10-30 12:01:55.050007 + 0800线程锁-18-10-30-0 [2783:84997] totalTicket:5.000000

2018-10-30 12:01:55.050007 + 0800线程锁-18-10-30-0 [2783:84998] totalTicket:4.000000

2018-10-30 12:01:55.050059 + 0800线程锁-18-10-30-0 [2783:84995] totalTicket:2.000000

从结果中,我们可以看出他的输出完全是乱序的,并不是正常我们认知下递减结果,是不安全的线程。

一,什么是线程安全?

       多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的比如:多个线程同时访问或读取同一共享数据,每个线程的读到的数据都是一样的,也就不存在线程不安全。如果多个线程对同一资源进行读写操作,那么每个线程读到的结果就是不可预料的,线程是不安全的。

        因此,线程安全,一定是对多线程而言的;单个线程,不存在线程安全问题。

二,多线程锁

1,自旋锁(OSSpinLock)

OSSpinLock不再安全这要因为低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程尝试获得这个锁,他会处于旋转锁的忙等状态,从而占用大量CPU。而低优先级线程无法与高优先级CPU时间,从而导致任务迟迟无法完成,无法释放锁定。如果还想使用自旋锁,除非开发者能保证访问锁的,否则iOS系统中所有类型的自旋锁都不能再使用了。

忙等这种自旋锁的实现原理:

do {  
    Acquire Lock
        Critical section  // 临界区
    Release Lock
        Reminder section // 不需要锁保护的代码
}

在Acquire Lock这一步,我们申请加锁,目的是为了保护临界区(Critical Section)中的代码不会被多个线程执行。

自旋锁的实现思路很简单,理论上来说只要定义一个全局变量,用来表示锁的可用情况即可,伪代码如下:


bool lock = false; /** 一开始没有锁上,任何线程都可以申请锁   */
do {  
    while(lock); /** 如果 lock 为 true 就一直死循环,相当于申请锁 */ 
    lock = true; /** 挂上锁,这样别的线程就无法获得锁 */ 
        Critical section  /** 临界区 */ 
    lock = false; /** 相当于释放锁,这样别的线程可以进入临界区 */
        Reminder section /** 不需要锁保护的代码  */        
}

初始化锁的全局变量,一开始是假的,而(锁定)的意思是,当锁定为真正的时候,就进行忙等死循环(DO-同时申请锁),由于一开始是假的,直接退出循环,然后锁锁上,执行临界区代码,也就是这个时候有其他线程访问,锁已经被锁上,而循环会一直忙等,处于申请锁状态,上一个锁执行完任务,就会解锁,这个时候锁定变成了假的,之前其他线程忙等状态下的条件变了,跳出循环,下一个线程执行锁=真,进门执行任务,其他线程继续等待。

为什么忙等会导致低优先级线程拿不到时间片?还得这从操作系统-的线程调度说起。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称RR)。每个线程会被分配一段时间片(量子),通常在10-100毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。

    OSSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    /** 需要执行的代码 */
    OSSpinLockUnlock(&lock);
   /** OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock)苹果在OSSpinLock注释表示被废弃,改用os_unfair_lock锁替代 */

2. 信号量(dispatch_semaphore)

信号量的实现原理比较简单,如果想要详细了解,可以参考我这篇文章GCD详解

dispatch_semaphore_t lock = dispatch_semaphore_create(1);/** 传入的参数必须大于或者等于0,否则会返回Null,如果想要当作线程锁使用,则必须设置信号量为1 */
long wait = dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); /** 信号量减1操作 */
/** 需要执行的代码 */
long signal = dispatch_semaphore_signal(lock);/** 信号量进行加1处理 */

⚠️dispatch_semaphore_signal与dispatch_semaphore_wait要成对出现,不然会抛出异常

3.互斥锁(pthread_mutex

互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

    pthread_mutex_t lock;
    pthread_mutex_init(&lock, NULL);
    pthread_mutex_lock(&lock);
    /** 需要执行的操作 */
    pthread_mutex_unlock(&lock);

4. NSCondition

NSCondition封装了一个互斥锁和条件变量,它把前者的  。互斥锁保证线程安全,条件变量保证执行顺序。lock 方法和后者的wait/signal 统一在 NSCondition 对象中,暴露给使用者

- (void) signal {
  pthread_cond_signal(&_condition);
}
 
/** 其实这个函数是通过宏来定义的,展开后就是这样 */
- (void)lock {
  int err = pthread_mutex_lock(&_mutex);
}

实践应用:

    NSCondition *condition = [[NSCondition alloc] init];
    NSMutableArray *products = [NSMutableArray array];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        NSLog(@"wait for product");
        [condition wait];/** 让当前线程处于等待状态 */
        [products removeObjectAtIndex:0];
        NSLog(@"produce count reduce,总量:%zi",products.count);
        [condition unlock];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [condition lock];
        [products addObject:[[NSObject alloc] init]];
        NSLog(@"produce count add,总量:%zi",products.count);
        [condition signal];/** CPU发信号告诉线程不用在等待,可以继续执行 */
        [condition unlock];
    });

输出结果为:

2018-10-30 16:49:57.377988 + 0800线程锁-18-10-30-0 [6573:223427]等待产品

2018-10-30 16:49:57.378200 + 0800线程锁-18-10-30-0 [6573:223425]产生计数加,总量:1

2018-10-30 16:49:57.378345 + 0800线程锁-18-10-30-0 [6573:223427]产生数量减少,总量:0

5. NSLock

NSLock是Objective-C以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了锁方法:

#define    MLOCK \
- (void) lock\
{\
  int err = pthread_mutex_lock(&_mutex);\
  // 错误处理 ……
}

NSLock在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。这里使用宏定义的原因是,OC内部还有其他几种锁,他们的锁方法都是一模一样,仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。

    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程1");
        sleep(10);
        [lock unlock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock tryLock]) {/** tryLock方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO */
            NSLog(@"线程2");
            [lock unlock];
        } else {
            NSLog(@"尝试加锁失败");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {/** lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。 */
            NSLog(@"线程3");
            [lock unlock];
        } else {
            NSLog(@"尝试加锁失败");
        }
    });

输出结果为:

2018-10-30 17:14:25.464806 + 0800线程锁-18-10-30-0 [6797:234890]线程1

2018-10-30 17:14:26.465822 + 0800线程锁-18-10-30-0 [6797:234891]尝试加锁失败

2018-10-30 17:14:35.469674 + 0800线程锁-18-10-30-0 [6797:234892]线程3

6. pthread_mutex(递归)

pthread_mutex(递归)是为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。

    pthread_mutex_t lock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    /**
      * PTHREAD_MUTEX_NORMAL 互斥锁不会检测死锁
      * PTHREAD_MUTEX_ERRORCHECK 互斥锁可提供错误检查
      * PTHREAD_MUTEX_RECURSIVE 递归锁
      * PTHREAD_MUTEX_DEFAULT 映射到 PTHREAD_PROCESS_NORMAL
      */
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_lock(&lock);
    /** 需要执行的代码 */
    pthread_mutex_unlock(&lock);

由于pthread_mutex有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。

7.递归锁(NSRecursiveLock)

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

递归锁也是通过pthread_mutex_lock函数来实现,在函数内部会判断锁的类型.NSRecursiveLock与NSLock的区别在于内部封装的pthread_mutex_t对象的类型不同,前者的类型为PTHREAD_MUTEX_RECURSIVE。

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            [lock lock];
            if (value > 0) {
                NSLog(@"value = %d", value);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
        RecursiveMethod(5);
    });

输出结果为:

2018-10-30 17:52:24.876873 + 0800线程锁-18-10-30-0 [7087:251693] value = 5

2018-10-30 17:52:24.877148 + 0800线程锁-18-10-30-0 [7087:251693] value = 4

2018-10-30 17:52:24.877207 + 0800线程锁-18-10-30-0 [7087:251693] value = 3

2018-10-30 17:52:24.877254 + 0800线程锁-18-10-30-0 [7087:251693] value = 2

2018-10-30 17:52:24.877296 + 0800线程锁-18-10-30-0 [7087:251693] value = 1

8.条件锁(NSConditionLock

NSConditionLock借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

- (id) initWithCondition:(NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}

它的 lockWhenCondition 方法其实就是消费者方法:

- (void)lockWhenCondition: (NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}

对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了所有的消费者:

- (void)unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}

实践应用:

 NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lockWhenCondition:1];
        NSLog(@"线程1");
        sleep(2);
        [lock unlock];
    });
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        if ([lock tryLockWhenCondition:0]) {/** 只有condition为0,才可以进行枷锁 */
            NSLog(@"线程2");
            [lock unlockWithCondition:2];/** 解锁,并设置condition为2 */
            NSLog(@"线程2解锁成功");
        } else {
            NSLog(@"线程2尝试加锁失败");
        }
    });
    
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);/** 以保证让线程2的代码先执行执行 */
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程3");
            [lock unlock];
            NSLog(@"线程3解锁成功");
        } else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(3);/** 以保证让线程2的代码先执行执行 */
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程4");
            [lock unlockWithCondition:1];
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });

输出结果为:

2018-10-31 10:10:3​​4.242631 + 0800线程锁-18-10-30-0 [3120:44938]线程2

2018-10-31 10:10:3​​4.242815 + 0800线程锁-18-10-30-0 [3120:44938]线程2解锁成功

2018-10-31 10:10:3​​5.242689 + 0800线程锁-18-10-30-0 [3120:45290]线程3

2018-10-31 10:10:3​​5.242864 + 0800线程锁-18-10-30-0 [3120:45290]线程3解锁成功

2018-10-31 10:10:3​​6.241514 + 0800线程锁-18-10-30-0 [3120:45291]线程4

2018-10-31 10:10:3​​6.241726 + 0800线程锁-18-10-30-0 [3120:45291]线程4解锁成功

2018-10-31 10:10:3​​6.241742 + 0800线程锁-18-10-30-0 [3120:45289]线程1

9. @同步

@Synchronized是一个OC层面的锁,通过牺牲性能换来语法上的简洁与可读。这是通过一个哈希表来实现的,OC在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁

@synchronized(/** 需要锁住的对象 */) {
     /** 需要加锁的代码 */
}

10.os_unfair_lock

os_unfair_lock iOS 10.0新推出的锁,用于解决OSSpinLock优先级反转问题。

    /** 初始化 */ 
    os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
    /** 加锁 */ 
    os_unfair_lock_lock(unfairLock);
    /** 解锁 */ 
    os_unfair_lock_unlock(unfairLock);
    /** 尝试加锁 */
    BOOL b = os_unfair_lock_trylock(unfairLock);

线程锁性能对比

ibireme 不再安全的OSSpinLock的一文中,有一张图片简单的比较了各种锁的加解锁性能:

文在中也。给出了测试代码,本人根据YY大神的提供的代码进行了最新的线程锁性能模拟测试

OSSpinLock:                   0.03 ms
dispatch_semaphore:           0.02 ms
pthread_mutex:                0.03 ms
NSCondition:                  0.02 ms
NSLock:                       0.03 ms
pthread_mutex(recursive):     0.03 ms
NSRecursiveLock:              0.04 ms
NSConditionLock:              0.08 ms
@synchronized:                0.12 ms
os_unfair_lock:               0.03 ms
---- fin (1000) ----
OSSpinLock:                   0.80 ms
dispatch_semaphore:           1.48 ms
pthread_mutex:                2.21 ms
NSCondition:                  2.55 ms
NSLock:                       2.66 ms
pthread_mutex(recursive):     3.35 ms
NSRecursiveLock:              4.40 ms
NSConditionLock:              7.04 ms
@synchronized:               10.88 ms
os_unfair_lock:               1.47 ms
---- fin (100000) ----
OSSpinLock:                  77.82 ms
dispatch_semaphore:         141.42 ms
pthread_mutex:              216.28 ms
NSCondition:                228.89 ms
NSLock:                     241.07 ms
pthread_mutex(recursive):   343.69 ms
NSRecursiveLock:            434.11 ms
NSConditionLock:            690.40 ms
@synchronized:             1034.34 ms
os_unfair_lock:             141.99 ms
---- fin (10000000) ----

从中不难看出OSSPinLock,dispatch_semaphore的性能远远优于其他的锁,但是OSSpinLock由于优先级反转的问题,苹果在iOS 10的时候推出了os_unfair_lock来替代,而且性能不减当年,但是要在iOS 10之后才能用(虽然自旋锁的性能优于互斥锁),而我们最常用的@synchronize明显性能最差,如果项目中对性能特别敏感,建议使用dispatch_semaphore,如果基于方便的话就用@synchronize就可以了。

参考资料1:HTTPS://blog.csdn.net/deft_mkjing/article/details/79513500

参考资料2:HTTPS://www.2cto.com/kf/201610/553687.html

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值