浅谈iOS开发中的“锁”

关于“锁”

iOS中的锁,也叫线程锁。是为了在多线程操作中,防止同一时间多个线程对共享资源进行读写操作而引入的一种机制。例如并行队列里的异步任务,有可能存在多个线程同时读写某一个对象的操作。如线程1正在执行写操作,此时线程2需要执行读操作,如此执行线程2获取到的数据肯定是一个错误的数据。因此,为了防止这种情况的发生,需要引入一种“在同一时间片段内只能有一个线程在对共享资源执行读写操作、而时间片段结束后能恢复到多个线程同时对共享资源执行读写操作的机制”。而这种机制,就是我们所说的锁。

“锁”的作用

在同一时间段片内、只允许一个线程对某一共享资源执行读写操作。

需要注意的是:锁只是改变了锁内共享资源的读写在时间片内只能有一个线程去访问,但并不会改变其它线程的执行。也就是说只有遇到多个线程同时访问共享资源的时候,线程只能一个一个的去执行。而其它没有访问到共享资源的线程,则依旧是并发执行的。

“锁”的种类

在iOS开发中,NSLock、@synchronized应该是我们接触的最早和做多的一种锁。至于NSConditionLock、NSRecursiveLock等一系列的锁,也会给大家一一介绍。

NSLock:普通锁,其用法可参见其头文件。

5

为2个方法1个属性,以及1个协议。2个方法分别为“尝试上锁”、“在某个时间之前锁住”;name则为锁的名字。其所包含的NSLocking协议,有2个方法,分别为“上锁”和“解锁”。

其基本用法如下所示:

//自定义并行队列
dispatch_queue_t queue = dispatch_queue_create("com.jiadai.demoOfLock", DISPATCH_QUEUE_CONCURRENT);
if (type == 0) {
    //NSLock
    NSLock *lock = [[NSLock alloc]init];
    __block int a = 0;
    dispatch_async(queue, ^{
        NSLog(@"线程1开始任务");
        [lock lock];
        for (int i = 0; i<5; i++) {
            sleep(1);
            a = a +1;
            NSLog(@"线程1执行任务a = %d",a);
            if (i==4) {
                sleep(1);
            }
        }
        [lock unlock];
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(queue, ^{
        NSLog(@"线程2开始任务");
        [lock lock];
        for (int i = 0; i<3; i++) {
            sleep(1);
            a = a +1;
            NSLog(@"线程2执行任务a = %d",a);
            if (i==2) {
                sleep(1);
            }
        }
        [lock unlock];
        NSLog(@"线程2解锁成功");
    });
    
}

这里自定义了一个并行异步线程,同事定义了一个共享资源a,并且给2个线程里的任务都给上锁。那么在上锁之前,2个任务理论上应该是同时启动的。当执行锁的操作时,由于只能允许一个线程执行任务,那么这里就有可能任务1在执行也有可能任务2在执行,总的来说只能有一个任务在执行。执行结束后,2个线程的任务都执行解锁操作,线程的任务又可以回到并行异步了。我们可参见执行的结果来进一步说明锁是怎么工作的。

5

可以看得到,在给线程1和线程2里的任务都上锁的情况下,同一时间片段内只能有一个任务执行,这就保证了操作的互斥性。也就是共享资源,单次只能执行一个a=a+1的操作。

如果我们去掉线程1或者线程2里的锁,或者去掉全部的锁,那么其执行,则会根据当前时间点拿到的a的值进行a=a+1的操作。

例如我们去掉线程2里的锁

dispatch_async(queue, ^{
    NSLog(@"线程2开始任务");
//    [lock lock];
    for (int i = 0; i<3; i++) {
        sleep(1);
        a = a +1;
        NSLog(@"线程2执行任务a = %d",a);
        if (i==2) {
            sleep(1);
        }
    }
//    [lock unlock];
    NSLog(@"线程2解锁成功");
});

其执行的结果:

5

显而易见,线程2的执行并没有因为线程1的锁而受到干扰。其执行时间与线程1一致,都是间隔1秒执行一次。其执行时的a,为其当前时刻拿到的a(可能先于线程1执行,也可能后于线程1执行)。同理线程1的执行,也没有受到线程2的影响。

对于tryLock和lockBeforeDate:(NSDate *)limit,2个方法都是返回BOOL值。tryLock表示尝试加锁,lockBeforeDate则表示在某个时间点之前加锁。返回YES则表示加锁成功,并在锁内执行方法;否则表示加锁失败,方法在无锁条件下执行。

@synchronized:其用法还是相当简单的,代码如下。

//@synchronized
__block int b1 = 0;
__block int b2 = 0;
dispatch_async(queue, ^{
    NSLog(@"线程1开始任务");
    @synchronized (self) {
        for (int i = 0; i<3; i++) {
            sleep(1);
            b1 = b1 +1;
            NSLog(@"线程1执行任务b1 = %d",b1);
            if (i==2) {
                sleep(1);
            }
        }
    }
});
dispatch_async(queue, ^{
    NSLog(@"线程2开始任务");
    @synchronized (self) {
        for (int i = 0; i<3; i++) {
            sleep(1);
            b2 = b2 +1;
            NSLog(@"线程2执行任务b2 = %d",b2);
            if (i==2) {
                sleep(1);
            }
        }
    }
});

在这里,我定义了2个异步任务和2个 对象b1、b2,并且执行+1操作。在加锁之前,2个任务应该是同时启动并开始执行;加锁之后,由于2个子线程的任务都是加锁执行的,那么在单位时间内只能一个一个的执行了。结果如下图所示:

5

可看到2个加锁的异步任务,在单位时间内是依次执行的。如果去掉其中一个线程的锁,则另一个线程的执行并不会受到加锁线程的影响。其执行效果,类似于NSLock。

NSConditionLock:条件锁,其用法可参见其头文件。

5

其用法跟NSLock类似,为tryLock和lockBeforeDate。但相对NSLock多了个conditon。我们可以通过具体实例来进一步了解NSConditionLock的用法。

//NSConditionLock
NSConditionLock *lock = [[NSConditionLock alloc]initWithCondition:0];
__block int c = 0;
dispatch_async(queue, ^{
    NSLog(@"线程1开始任务");
    [lock lockWhenCondition:0];
    for (int i = 0; i<3; i++) {
        sleep(1);
        c = c +1;
        NSLog(@"线程1执行任务c = %d",c);
    }
    [lock unlockWithCondition:1];
});
dispatch_async(queue, ^{
    NSLog(@"线程2开始任务");
    [lock lockWhenCondition:1];
    for (int i = 0; i<3; i++) {
        sleep(1);
        c = c +1;
        NSLog(@"线程2执行任务c = %d",c);
    }
    [lock unlockWithCondition:2];
});
dispatch_async(queue, ^{
    NSLog(@"线程3开始任务");
    [lock lockWhenCondition:2];
    for (int i = 0; i<3; i++) {
        sleep(1);
        c = c +1;
        NSLog(@"线程3执行任务c = %d",c);
    }
    [lock unlockWithCondition:3];
});

首先我们创建了一个条件锁,并且设定起始条件condition=0。随后创建了3个异步任务,并且其任务是在锁内执行的。所以,3个线程锁内的任务,在同一时间片段内只能有一个在执行。当condition=0的时候,线程1的任务加锁,并且在线程1的任务执行完毕后才执行解锁操作。在解锁的时候,给condition重新赋值。而此时线程2满足加锁条件,则线程2的任务会被立即执行。同理,当其它线程满足加锁操作时,则其内的任务也会被立即执行。需要注意的是,加锁和解锁需成对出现,否则会造成一些错误。

其运行结果如下图所示:

5
由于NSConditionLock也遵循NSLocking协议,所以可像NSLock那样直接使用lock和unlock对任务执行加锁解锁操作。

对于其它几个方法,需要根据返回的BOOL值进行判断是否加锁成功。如果加锁成功,则在方法执行完后进行解锁。若没有成功,那就不需要解锁,否则会造成一些错误。例如try加锁成功,则执行锁的方法,结束后进行解锁。加锁失败,则另行处理。

NSRecursiveLock:递归锁,其用法可参见其头文件。

5

其用法与NSConditionLock类似,为tryLock和lockBeforeDate。

//NSRecursiveLock
NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
dispatch_async(queue, ^{
    static void (^MyMethod)(int);
    MyMethod = ^(int val){
        [lock lock];
        if (val > 0) {
            sleep(1);
            NSLog(@"执行任务val=%d",val);
            MyMethod(val-1);
        }
        [lock unlock];
    };
    MyMethod(5);
});

例如递归方法MyMethod,内有有判断,当val>0的时候,则会重新调用自身。如果是普通锁,则会先执行lock,后又执行MyMethod,并尝试lock。但由于该线程内已经有一个lock在执行任务,重新加锁则会造成死锁。此时选用递归锁,则允许该线程可被多次lock,并且在执行unlock的时候,移除所有的lock。

其运行结果如下图所示:

5

若将NSConditionLock换成普通的NSLock,其执行结果如下:

5

当执行第二次MyMethod时,发生死锁(当前线程已存在一个锁,无法再添加新的锁)。

NSCondition:条件锁,其用法可参见其头文件。

5

其主要提供2个方法,wait(等待)和signal(发送信号)。同时遵循NSLocking协议,所以可直接使用lock和unlock方法。

其主要特点为线程任务等待发送信号唤醒等待中的任务。具体用法如下所示:

//NSCondition
__block int d = 0;
NSCondition *lock = [[NSCondition alloc]init];
dispatch_async(queue, ^{
    //上锁,并等待
    [lock lock];
    if (d == 0) {
        //等待
        NSLog(@"线程等待");
        [lock wait];
    }
    //执行任务2
    d = d+1;
    NSLog(@"执行条件任务2d=%d",d);
    //解锁
    [lock unlock];
});
dispatch_async(queue, ^{
    //上锁
    [lock lock];
    d = d + 1;
    //执行任务1
    NSLog(@"执行线程任务1d=%d",d);
    //发送信号,通知等待中的线程
    NSLog(@"发送信号");
    [lock signal];
    //解锁
    [lock unlock];
});

首先,我们创建了一个条件锁,并初始化一个数据d = 0。然后我们创建了2个异步任务,在线程2里,进行判断:如果d = 0,那么线程等待;否则执行线程2任务。在线程1里,我们给任务上锁,并执行d = d+1的操作。操作完成后,发送信号激活处于等待的任务并对线程1进行解锁。而此时,线程2在收到信号后,在线程1解锁完成后,执行内部任务,并进行线程2解锁。从而完成整个加锁、等待、执行任务、发送信号、执行任务、解锁等过程。

对于NSCondition,我们重点需要掌握其线程等待和发送信号激活等待线程等方法。

OSSpinLock:自旋锁

需要导入头文件:

#import <libkern/OSAtomic.h>

具体用法如下图所示:

//OSSpinLock
__block OSSpinLock lock = OS_SPINLOCK_INIT;
__block int e = 0;
dispatch_async(queue, ^{
    OSSpinLockLock(&lock);
    for (int i = 0; i<3; i++) {
        sleep(1);
        e = e +1;
        NSLog(@"线程1执行任务e = %d",e);
    }
    OSSpinLockUnlock(&lock);
});
dispatch_async(queue, ^{
    OSSpinLockLock(&lock);
    for (int i = 0; i<3; i++) {
        sleep(1);
        e = e +1;
        NSLog(@"线程2执行任务e = %d",e);
    }
    OSSpinLockUnlock(&lock);
});

其执行结果如下:

5

可见其用法及执行结果,与NSLock类似。但OSSpinLock已不在安全,在iOS10中首次被遗弃(不建议使用)。

pthread_mutex:互斥锁

需要导入头文件:

#import <pthread.h>

具体用法如下图所示:

//pthread_mutex 互斥锁
static pthread_mutex_t lock;
__block int f = 0;
pthread_mutex_init(&lock,NULL);
dispatch_async(queue, ^{
    pthread_mutex_lock(&lock);
    for (int i = 0; i<3; i++) {
        sleep(1);
        f = f +1;
        NSLog(@"线程1执行任务f = %d",f);
    }
    pthread_mutex_unlock(&lock);
});
dispatch_async(queue, ^{
    pthread_mutex_lock(&lock);
    for (int i = 0; i<3; i++) {
        sleep(1);
        f = f +1;
        NSLog(@"线程2执行任务f = %d",f);
    }
    pthread_mutex_unlock(&lock);
});

其执行结果如下:

5

可见其用法及执行结果,与NSLock类似,加锁解锁成对出现。

pthread_mutex(recursive):递归锁。

与pthread_mutex类似,使用场景与NSRecursiveLock相同,都是用于单个线程内多次进行加锁操作。具体用法如下所示:

static pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用

dispatch_async(queue, ^{
    static void (^MyMethod)(int);
    MyMethod = ^(int val){
        pthread_mutex_lock(&lock);
        if (val > 0) {
            sleep(1);
            NSLog(@"执行任务val=%d",val);
            MyMethod(val-1);
        }
        pthread_mutex_unlock(&lock);
    };
    MyMethod(5);
});

其执行结果如下:

5

dispatch_semaphore:信号量。

其用法与NSCondition有些类似:等待、发送信号、唤醒等待的任务。具体使用如下所示:

dispatch_semaphore_t signal = dispatch_semaphore_create(2);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
dispatch_async(queue, ^{
    dispatch_semaphore_wait(signal, overTime);
    sleep(1);
    NSLog(@"执行线程1任务");
    dispatch_semaphore_signal(signal);
});
dispatch_async(queue, ^{
    dispatch_semaphore_wait(signal, overTime);
    sleep(1);
    NSLog(@"执行线程2任务");
    dispatch_semaphore_signal(signal);
});
dispatch_async(queue, ^{
    dispatch_semaphore_wait(signal, overTime);
    sleep(1);
    NSLog(@"执行线程3任务");
    dispatch_semaphore_signal(signal);
});

其执行结果如下:

5

需要注意的是:在信号量创建的时候,需要带上一个并发执行数。例如此处我设置的是1,那单位时间片内只能有一个线程在执行。同时,线程等待需要传入等待时间;表示在等待多少时间后,就会执行线程方法。

性能对比

锁的性能,如下图所示:

5

具体可参见ibireme大神的文章《不再安全的OSSpinLock》。

总结

对于iOS中的锁,需要理解锁的概念和锁的作用:是一种解决多线程中共享资源间的互斥访问的一种机制;锁的使用,会降低多线程访问同一资源时执行的效率,但是很好的解决了不同线程安全的访问同一资源的问题;需要注意的是,同一时间片内,只能有一个加锁的任务在执行。而那些没有加锁的并行异步线程任务,则仍是多个线程同时执行的;对于锁,还需要记住常用的几种锁以及各种锁之间的差异与使用场景,这对于我们在多线程开发中对于资源的安全性将会更有帮助。Demo可见我的Github

以上是我对iOS中锁的一些个人理解,仅做参考之用。

参考

简书:iOS 开发中的八种锁(Lock)

网易博客:关于@synchronized(self)的用法

百度百科:互斥

ibireme:不再安全的OSSpinLock

 

原文链接:https://blog.caichenghan.com/blog/12.shtml

posted @ 2017-02-15 10:34  一半清醒一半醉  阅读(281)  评论(0)    收藏  举报