iOS线程安全——锁(二)

简介: iOS线程安全——锁(二)

信号量


在iOS开发中,信号量就是通过GCD来实现的,而GCD是对C语言的一个封装,不同的开发语言中对于信号量semaphore都有自己的实现,所以本节不仅是代表了pthread,也是信号量的使用,更是跨线程访问的一个主要的知识点。

信号量的使用其实很简单,与其他开发语言中使用的信号量类似,通过对信号的等待和释放来使用,信号量属于生产者消费者模式,这种模式可以用在多个使用场景中,下面只是比较常见的一种。

- (void)semaphoneTest {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    __block Person *p = [Person new];
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i <totalCount; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            p.age++;
            dispatch_semaphore_signal(semaphore);
        }
        NSLog(@"% zd\n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            p.age++;
            dispatch_semaphore_signal(semaphore);
        }
        NSLog(@"% zd \n",p.age);
    }];
}


每次在访问p.age之前,都会等待一个信号量,才能实现对age的访问,在创建方法中,需要传一个value的long类型的值,表示总共已经有了多少个信号可以使用。

举个简单的例子,办理银行业务时,银行可以设置多个窗口,这个窗口数就是信号量创建的这个value值。假设银行总共有5个窗口,相当于总共有5个资源,每有一个客户去窗口办理业务的时候,相当于消费了这个资源,当5个窗口都有客户办理业务的时候,也就是没有剩下的可用资源,那么这时候如果还有人要办理业务就得等某个窗口空出来,窗口空出来相当于释放了一个资源,这样才能接着被下一个客户使用。这就是生产者消费者理论,生产者消费者理论在多线程事件调度方面可以起到很强大的作用。如果分配的资源只有一个的时候,那么就是本节中的例子。还是刚刚举的例子,客户在同一个窗口无论如何存钱取钱都不会导致钱的数目出现差错。

所以说,当资源充足时,当代码执行到dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER)时,不用等待,而直接消耗一个资源,只有在当前没有可用的资源时,才会等待dispatch_semaphore_signal(semaphore)来释放一个可用的资源。这其中的逻辑并不复杂,就是事务竞争资源。

后面还有一个时间参数,这个比较简单,在消费者等待一个可用的资源时是有时间限制的,超过该时间就不去等待资源而直接执行下面的代码。这在此处或许有些不合逻辑,因为假设时间很小,在没有获取到信号量资源的时候就去执行代码,可能会造成非线性安全的事故,但这是系统安全的,也就是并不会造成应用崩溃。在本节关于锁的内容中,将时间设置为DISPATCH_TIME_FOREVER,表示将一直会等下去,这就确保了线程的安全。


NSConditionLock与NSCondition


1.NSConditionLock


状态锁是一种比较常用的锁,在多线程操作中,用户可以指定某线程去执行操作,只需要设定对应的状态即可。

- (void)conditionLockTest {
    NSConditionLock *lock = [[NSConditionLock alloc] init];
    __block Person *p = [Person new];
    NSInteger thread1 = 1;
    NSInteger thread2 = 0;
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lockWhenCondition:thread1];
            p.age++;
            [lock unlockWithCondition:thread2];
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i ++) {
            [lock lockWhenCondition:thread2];
            p.age++;
            [lock unlockWithCondition:thread1];
        }
        NSLog(@"% zd \n",p.age);
    }];
}


NSConditionLock主要有两个方法,一个是-lockWhenCondition:,一个是-unlockWithCondition:。用法很简单,表示只有在某种状态下才能上锁,操作完成后解锁并将状态更改,供下次符合条件的线程上锁。举个简单的例子,有一个男女公用的厕所,一次只能有一个人使用,厕所门上有一个标示牌,当牌子上是♂的时候,表示这个厕所现在只能男生使用,即使是女生排在第一位(CPU系统调度,但状态不符合),所以得一直找到男生,才能使用。当男生使用完,可以将厕所的标示牌随意更改为♂或者♀,接下来同上。这就是NSConditionLock的作用,在例子中,两个线程在操作完成后将状态值更改为其他值,所以两个线程能够轮流执行,通过打印结果也可以看出来,二者只相差1。


2.NSCondition


这里介绍一下与NSConditionLock类似的NSCondition,看起来两个差不多,虽然只相差一个Lock,但足以表示它们的主要用法不同。NSConditionLock在刚刚已经介绍过,NSCondition更类似于信号量的使用。虽然NSConditionLock与NSCondition在用法上略有不同,但为了达到与NSConditionLock相同的用法,这里展示与NSConditionLock做相同的事。

在展示NSCondition代码之前,我们先看一下Apple的官方文档中,对于NSCondition提供的一段伪代码。

lock the condition
while(!(boolean_predicate)) {
    wait on condition
}
do protected work
(optionally, signal or broadcast the condition again or change a predicate value)
unlock the condition


通过伪代码可以看出,在使用NSCondition时,先将其上锁,当其不满足条件时,使其处于wait状态,紧接着写上一些需要做线程安全的代码,然后释放信号量,或者广播一个状态,足以break刚才的while循环,最后将其解锁。都是根据条件来上锁解锁,为了达到和NSConditionLock相同的效果,下面是展示代码。

- (void)conditionTest {
    NSCondition *lock = [[NSCondition alloc]init];
    __block Person *p = [Person new];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lock];
            while (p.age % 2 == 0) {
                [lock wait];
            }
            p.age++;
            [lock signal];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lock];
            while (p.age % 2 == 1) {
                [lock wait];
            }
            p.age++;
            [lock signal];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
}


当p.age是单数时,线程1将处于wait状态,并且由线程2执行,反之当p.age是偶数时,线程2处于wait状态,由线程1执行,达到了与NSConditionLock例子中两个线程轮流执行的效果。通过打印,也是可以得出该结论的。


自旋锁


自旋锁在iOS系统中的实现是OSSpinLock。自旋锁通过一直处于while盲等状态,来实现只有一个线程访问数据。由于一直处于while循环,所以对CPU的占用也是比较高的,用CPU的消耗换来的好处就是自旋锁的性能很高。

然而现在不建议使用自旋锁,因为自旋锁在iOS中有bug,这个稍后将讲到,下面先介绍一下OSSpinLock的用法,虽然现在基本上不使用它。

#import <libkern/OSAtomic.h>
- (void)OSSpinLockTest {
    __block OSSpinLock spinLock = OS_SPINLOCK_INIT;
    __block Person *p = [Person new];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            OSSpinLockLock(&spinLock);
            p.age++;
            OSSpinLockUnLock(&spinLock);
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            OSSpinLockLock(&spinLock);
            p.age++;
            OSSpinLockUnLock(&spinLock);
        }
        NSLog(@"% zd \n",p.age);
    }];
}


可以看到自旋锁的使用也是很简便的,首先需要#import <libkern/OSAtomic.h>,因为关于自旋锁的API是在这个文件中声明的。创建自旋锁也是通过一个静态宏,在线程内通过OSSpinLockLock和OSSpinLockUnlock来上锁、解锁。如果不是因为现在的OSSpinLock出现了使用bug,在性能以及使用方面来说,都是很好的使用锁的选择。下面来详细说下自旋锁。

为何自旋锁现在出现bug呢?在最近的iOS操作系统中,实现的自旋锁与自身维护线程的调度算法有冲突,是导致bug的原因。在iOS维护的线程中,有一套调度算法,会使高优先级的线程优先执行。所以当低优先级的线程获取到了自旋锁,高优先级的线程想要申请该锁,就会使高优先级线程处于while一直循环申请的状态,与低优先级的线程处理抢夺CPU处理时间,导致高优先级不能申请成功,造成死锁的状态,并且两者都不能释放。目前针对这种情况也是有处理方法的,但就会使自旋锁的使用稍显麻烦,这里不做阐述。

由于自旋锁出现了这个问题,导致在目前的开发中,很少有开发者会选择OSSpinLock来实现锁的功能,即使OSSpinLock的性能在各种所有锁的性能中是最好的,所以需要慎用。


递归锁


前面简单提及递归锁的概念,说到递归,在很多代码以及算法中某函数内部会调用自身,通过这种形式,将比较复杂的问题分解为稍容易一些的问题,再通过相同的方法来继续处理,同理一层一层分解,然后将每个返回值返回至上一层,层层返回达到最终结果。一些比较经典的关于递归的例子就是斐波那契函数、二叉树遍历等。

上面简单介绍了一下递归的概念,为下面介绍递归锁做个铺垫。其实递归锁跟递归并没有太大的关系,只是有相似的使用模型,在以上介绍锁的代码中,一个锁只是请求一份资源,而在一些开发实际中,往往需要在代码中嵌套锁的使用,也就是在同一个线程中,一个锁还没有解锁就再次加锁,这在代码编译器中不会报错以及警告,但是运行期会直接出现问题,并且不执行锁中代码。

// 这是错误代码!
- (void)wrongRecursiveTest {
    NSLock *lock = [[NSLock alloc] init];
    __block Person *p = [Person new];
    NSLock(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (i = 0; i < totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
}


这是最基本的锁NSLock,我们依次举例,来表示在同一线程中多次上锁,类似于递归中在一组加锁解锁中再次加锁解锁。从代码中可以看到,连续调用了两次加锁、解锁,这只是为了达到演示目的,实际开发中这两次加锁中间可能会有其他代码,是手误也好,是业务需求使然也罢,在运行之后可以看到,并没有打印p.age的两次结果,取而代之的是一段错误log:

*** -[NSLock lock]: deadlock (<NSLock: 0x6180000d1c60>'(null)')
*** Break on _NSLockError() to debug.


可以通过控制台打印得到信息,我们在同一线程重复上锁时,会造成死锁,系统在debug模式下会自动break这段代码。这并不是我们想要的,因为已经影响了正常的代码执行,如果在业务中出现就会造成不可知的后果。以下是正确的递归锁使用代码。

- (void)recursiveTest {
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    __block Person *p = [Person new];
    NSLog(@"begin");
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"% zd \n",p.age);
    }];
}


当我们将NSLock换成了NSRecursiveLock,在这种递归锁下运行,可以看到代码是如期正常执行的。虽然代码执行成功了,读者可能仍然会有些困惑,还是不能明白为何递归锁会这么写,感觉在实际开发中并没有什么可以借鉴的使用场景。下面对递归锁再次举个例子。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value) {
    [theLock lock];
    if(value != 0) {
        --value;
        MyRecursiveFunction(value);
    }
    [theLock unlock];
}
MyRecursiveFunction(5);


在这样的代码中或许读者能明白关于递归锁的实际使用场景,递归锁的使用在实际开发中也是常有的,所以需要谨慎。


小结


在本章介绍的这些锁中,可以应用于实际开发中的绝大部分使用场景,每种功能可以根据需求使用不同的锁来实现,而同一种锁根据其特性能发挥出不同的使用效果。这里重点提及一下pthread,pthread是一套跨平台的多线程API,其内部提供了丰富的API可以使用,而且NSLock以及NSConditionLock等都是基于pthread的实现,之所以将其并列出来讲解仅仅是为了阐述其锁的功能。关于pthread也只是介绍了其关于锁的一部分,可见其多么强大。而对于各个锁的优缺点在每小点中也有阐述,包括自旋锁的不安全性等,开发者应该对其有一定的了解。

在APP开发日益复杂庞大的今天,多线程的使用能有效提高应用的事务处理能力,开发者在享受多线程带来的便利的同时,也要注意多线程衍生出的线程安全问题。

目录
相关文章
|
2月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
2月前
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
高并发编程之多线程锁和Callable&Future 接口
28 1
|
2月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
71 3
|
11天前
|
算法 Java 编译器
【JavaEE多线程】掌握锁策略与预防死锁
【JavaEE多线程】掌握锁策略与预防死锁
20 2
|
11天前
|
安全 Java 编译器
【JavaEE多线程】线程安全、锁机制及线程间通信
【JavaEE多线程】线程安全、锁机制及线程间通信
31 1
|
12天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
15天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
246 2
|
16天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
42 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
35 0