iOS中的锁

前言

上一话题我理解的GCD最后留下的一个疑问:

如果项目需要在不阻塞线程的情况下,并发地对一个变量进行操作。小伙伴们能想到会造成什么后果吗?要用什么方法对这个问题进行解决呢?

本章就来说一下对这个问题的认识。

一、什么是线程安全

我理解的线程安全很简单,就是多个线程里的队列任务并发同时访问同一个数据。这种情况下,就会造成线程安全。而我们描述的问题其实就是这么一个情况。

二、流行的卖票实验

这个实验非常适合学习掌握多线程下的线程安全。其原理大家应该都有过了解。即开启新线程,对同一个数据源(票票)进行访问操作。我们需要保证卖票的效率的同时,也要保证能够正常卖出票。
要保证卖票的效率,我们最好能够开辟一个新线程,保证主线程不阻塞。所以一开始就要确定使用**异步派发async**。

  1. 异步派发+串行队列进行卖票
// 假设有10张票
self.tickets = 10;

// 创建一个串行队列,并开启两个任务进行卖票
- (void)test1
{
    // 创建串行队列
    dispatch_queue_t squeue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
    dispatch_async(squeue, ^{
        
        [self dealTask1];
    });
    dispatch_async(squeue, ^{
        
        [self dealTask1];
    });
}

// 任务
- (void)dealTask1
{
    while (true) {
        [NSThread sleepForTimeInterval:0.5];
        if (self.tickets > 0)
        {
            self.tickets --;
            NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"卖光了");
            // 逃离任务
            break;
        }
    }
}

上面的案例输出是正常的,票能够正常卖出,且不阻塞主线程。但是因为是串行队列,所以两个任务都是在同一个新线程下进行处理。所以效率可见一斑。

  1. 异步派发+并行队列进行卖票
// 创建并行队列,并开启两个任务进行卖票。
- (void)test2
{
    // 创建并行队列
    dispatch_queue_t cqueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(cqueue, ^{
        
        [self dealTask1];
    });
    dispatch_async(cqueue, ^{
        
        [self dealTask1];
    });
}

输出如下:

2018-03-21 14:53:21.832629+0800 XLLLockTest[13563:292378] 剩余票数--8张--{number = 4, name = (null)}
2018-03-21 14:53:21.832696+0800 XLLLockTest[13563:292379] 剩余票数--9张--{number = 3, name = (null)}
2018-03-21 14:53:22.336391+0800 XLLLockTest[13563:292379] 剩余票数--6张--{number = 3, name = (null)}
2018-03-21 14:53:22.336426+0800 XLLLockTest[13563:292378] 剩余票数--7张--{number = 4, name = (null)}
2018-03-21 14:53:22.840266+0800 XLLLockTest[13563:292379] 剩余票数--5张--{number = 3, name = (null)}
2018-03-21 14:53:22.840260+0800 XLLLockTest[13563:292378] 剩余票数--5张--{number = 4, name = (null)}

根据log来看,卖票出错了。原因就是因为我们开启了并行队列。所以两个任务是在两个线程下同时执行的。而执行任务时操作的是同一个数据源self.sockets。所以会导致线程安全问题。

那我们就会想能不能有方法,在不同线程访问同一个数据源,依然能够保持数据同步呢?

三、锁

是最常用的同步工具。一段代码段(任务)在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。这样保证了多线程并发执行任务的同时,同一变量在不同时间被访问。

  • NSLock 对象锁
    NSLock是最简单的一个互斥锁,在可能在多个线程执行的队列任务前进行加锁,任务执行结束后进行解锁。这样就能够保证这个任务的线程安全。
    需要注意的是,加锁与解锁是相互对应的。如果多加了一层锁,会造成死锁现象。后面的队列因为这个多余的、不能解的锁而一直不会被执行。
- (void)dealTask2
{
    [_lock lock];
    while (true) {
        [NSThread sleepForTimeInterval:0.5];
        if (self.tickets > 0)
        {
            self.tickets --;
            NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"卖光了");
            // 逃离任务
            break;
        }
    }
    [_lock unlock];
}
  • @synchronized 互互斥锁
    @synchronized也能够保证线程安全,同时也有它的优点:不用显示的去创建锁对象。一般会使用self来加锁,注意这个对象必须是全局唯一的,多个线程同时访问的时候,必须保证@synchronize(OC对象)这个OC对象是相同的。不过@synchronized代码块的性能不敢恭维。
- (void)dealTask3
{
    @synchronized(self) {
        while (true) {
            [NSThread sleepForTimeInterval:0.5];
            if (self.tickets > 0)
            {
                self.tickets --;
                NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"卖光了");
                // 逃离任务
                break;
            }
        }
    }
}
  • dispatch_semaphore_t 信号量
    信号量对于GCD技术开发的时候,用的还是很频繁的。它的性能也不差。使用的时候,肯定是遵循我下面总结的原则:
  1. 每个队列任务执行的时候,如果发现信号量为1,则-1执行任务。如果信号量为0,则等待执行。
  2. 队列任务执行之后,需要single一下,信号量+1。
  3. 信号量waittime是可以自定义的。如果time设为3秒,则即使信号量不为1,3秒后也会执行该任务。不过一般为了保证线程安全,time都设为DISPATCH_TIME_FOREVER
    所以当信号等待时间设为DISPATCH_TIME_FOREVER之后,如果不遵循前两条原则,极有可能造成死锁。
// 首先将信号量初始为1,保证第一个队列任务能够正常执行
_semaphore = dispatch_semaphore_create(1);

- (void)dealTask4
{
    // dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, 3);
    // 等待信号量为1,然后执行wait,信号量-1.组织其他线程任务同时执行
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    while (true) {
        [NSThread sleepForTimeInterval:0.5];
        if (self.tickets > 0)
        {
            self.tickets --;
            NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"卖光了");
            // 逃离任务
            break;
        }
    }
    // 队列任务执行完成,信号量+1,使下一个任务正常被执行
    dispatch_semaphore_signal(_semaphore);
}
  • pthread_mutex 互斥锁
    性能很好,并且在使用递归的时候,也可以当做递归锁来使用。
    使用前,需导入头文件#import
// 初始化互斥锁
pthread_mutex_init(&_pthread, NULL);

- (void)dealTask5
{
    // 加锁
    pthread_mutex_lock(&_pthread);
    while (true) {
        [NSThread sleepForTimeInterval:0.5];
        if (self.tickets > 0)
        {
            self.tickets --;
            NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"卖光了");
            // 逃离任务
            break;
        }
    }
    // 解锁
    pthread_mutex_unlock(&_pthread);
}
  • OSSpinLock 自旋锁
    它是性能最好的锁,但是目前苹果发现了它有bug,并不能完全保证线程安全,已经将其修饰了os_unfair_lock。所以不建议使用。

在此插一句
YYKit开源框架作者也因为得知OSSpinLock自旋锁的漏洞,已经将项目中的自旋锁换上性能差不多的pthread_mutex互斥锁。
文章:不再安全的OSSpinLock

使用时需要导入头文件#import

// 1. 初始化自旋锁
_spinLock = OS_SPINLOCK_INIT;
- (void)dealTask6
{
    // 加锁
    OSSpinLockLock(&_spinLock);
    while (true) {
        [NSThread sleepForTimeInterval:0.5];
        if (self.tickets > 0)
        {
            self.tickets --;
            NSLog(@"剩余票数--%zd张--%@",self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"卖光了");
            // 逃离任务
            break;
        }
    }
    // 解锁
    OSSpinLockUnlock(&_spinLock);
}

其次还有诸如NSRecursiveLock递归锁NSCondition,NSConditionLock条件锁等等。用的比较少,也没过多了解。

四、总结

iOS保证线程安全的锁有很多,根据自己需要选择合适的锁,以上介绍的几个一般的项目足够用了。网上有好多人对各个锁进行了性能测试,我这边截取一下在YYKit作者文章下看到的性能排行图,供大家参考。

iOS中的锁_第1张图片
线程锁性能排行榜

这是Demo,包含对上一章同步异步+串行并行的探索代码
参考文章: https://www.jianshu.com/p/35dd92bcfe8c
写得不好,但都是一步一步走下来得到的结果。对于初级的认识还是很有帮助的。如果有发现不对的地方,希望一定要指正出来,共同进步。

你可能感兴趣的:(iOS中的锁)