iOS开发基础——线程安全(进程锁)

栏目: IOS · 发布时间: 5年前

内容简介:锁:是保证线程安全常见的同步工具,防止Data race(数据竞争)的发生。其中,锁的属性包含一下四种:实例代码:

在IOS上进行多线程开发,为了保证线程安全,防止资源竞争,需要给进程进行加锁,通常用到的进程锁分为7种。

  • 信号量
  • 互斥锁
  • 自旋锁
  • 递归锁
  • 条件锁
  • 读写锁
  • 分布式锁

锁:是保证线程安全常见的同步工具,防止Data race(数据竞争)的发生。

Data race(数据竞争):

  • 两个或者更多线程在一个程序中,并发的访问同一数据
  • 至少一个访问是写入操作
  • 些线程都不使用任何互斥锁来控制这些访问

pthread_mutex

pthread_mutexattr_t attr;  
pthread_mutexattr_init(&attr);  
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性

pthread_mutex_t mutex;  
pthread_mutex_init(&mutex, &attr) // 创建锁
pthread_mutex_lock(&mutex); // 申请锁  
pthread_mutex_unlock(&mutex); // 释放锁  
复制代码

其中,锁的属性包含一下四种:

PTHREAD_MUTEX_NORMAL:默认值普通锁,当一个线程加锁以后,其他线程进入按照优先顺序进入等待队列,并且解锁的时候按照先入先出的方式获得锁。
PTHREAD_MUTEX_ERRORCHECK:检错锁,当同一个线程获得同一个锁的时候,则返回EDEADLK,否则与普通锁处理一样。
PTHREAD_MUTEX_RECURSIVE:递归锁。这里有别于上面的检错锁,同一个线程可以递归获得锁,但是加锁和解锁必须要一一对应。
PTHREAD_MUTEX_DEFAULT:适应锁,等待解锁之后重新竞争,没有等待队列。
复制代码

信号量

dispatch_semaphore 是GCD用来同步的一种方式, dispatch_semephore_create 方法用户创建一个 dispatch_semephore_t 类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。 dispatch_semaphore_wait 方法是等待一个信号量,该方法会判断 signal 的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和 NSCondition 一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且 dispatch_semaphore_signal 释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。

dispatch_semaphore_create(long value)
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
dispatch_semaphore_signal(dispatch_semaphore_t dsema)

实例代码:

- (void)semaphoreSync {
    NSLog(@"semaphore---begin");
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建初始信号量 为 0 ,阻塞所有线程
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务A
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        number = 100;
        // 执行完线程,信号量加 1,信号总量从 0 变为 1
        dispatch_semaphore_signal(semaphore);
    });
    //原任务B
    ////若计数为0则一直等待,直到接到总信号量变为 >0 ,继续执行后续代码
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
复制代码

互斥锁

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

当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。

当临界区加上互斥锁以后,其他的调用方不能获得锁,只有当互斥锁的持有方释放锁之后其他调用方才能获得锁。

调用方在获得锁的时候发现互斥锁已经被其他方持有,那么该调用方只能进入睡眠状态,这样不会占用CPU资源。但是会有时间的消耗,系统的运行时基于CPU时间调度的,每次线程可能有100ms的运行时间,频繁的CPU切换也会消耗一定的时间。

NSLock:

NSLock遵循NSLocking协议,同时也是互斥锁,提供了 lockunlock 方法来进行加锁和解锁。 NSLock内部是封装了 pthread_mutext ,类型是 PTHREAD_MUTEXT_ERRORCHECK ,它会损失一定的性能换来错误提示。

- (void)lock;  
- (void)unlock; 
- (BOOL)tryLock;  
- (BOOL)lockBeforeDate:(NSDate *)limit;  
复制代码

tryLocklock 方法都会请求加锁,唯一不同的是 trylock 在没有获得锁的时候可以继续做一些任务和处理。 lockBeforeDate: 方法也比较简单,就是在limit时间点之前获得锁,没有拿到锁就返回NO。

@synchronized:

这其实是一个 OC 层面的锁,防止不同的线程同时执行同一段代码,相比于使用 NSLock 创建锁对象、加锁和解锁来说, @synchronized 用着更方便,可读性更高。

大体上,想要明白 @synchronized ,需要知道在 @synchronizedobjc_sync_enterobjc_sync_exit 的成对调用,而且每个传入的对象,都会为其分配一个递归锁并存储在哈希表中。在 objc_sync_enter 中加锁,在 objc_sync_exit 中解锁。

具体可以参考这篇文章: 关于 @synchronized,这儿比你想知道的还要多

@synchronized(self) {  
    //数据操作  
}
复制代码

自旋锁

自旋锁的目的是为了确保临界区只有一个线程可以访问。

当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放,是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

由于调用方会一直循环看该自旋锁的的保持者是否已经释放了资源,所以总的效率来说比互斥锁高。但是自旋锁只用于短时间的资源访问,如果不能短时间内获得锁,就会一直占用着CPU,造成效率低下。

OSSpinLock:

自旋锁的一种,由于在某些场景下不安全已被弃用。 需导入头文件 #import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;  
OSSpinLockLock(&lock);  
OSSpinLockUnlock(&lock);
OSSpinLockTry(&lock);
复制代码

自旋锁存在优先级反转问题, OSSpinLock 是自旋锁,也正是由于它是自旋锁,所以容易发生优先级反转的问题。在ibireme的文章中已经写到,当一个低优先级线程获得锁的时候,如果此时一个高优先级的系统到来,那么会进入忙等状态,不会进入睡眠,此时会一直占用着系统CPU时间,导致低优先级的无法拿到CPU时间片,从而无法完成任务也无法释放锁。除非能保证访问锁的线程全部处于同一优先级,否则系统所有的自旋锁都会出现优先级反转的问题。现在苹果的 OSSpinLock 已经被替换成

os_unfair_lock 
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
复制代码

os_unfair_lock:

os_unfair_lock 是苹果官方推荐的替换 OSSpinLock 的方案,用于解决 OSSpinLock 优先级反转问题,但是它在iOS10.0以上的系统才可以调用。

导入头文件 #import< os/lock.h >

os_unfair_lock_t unfairLock;  
unfairLock = &(OS_UNFAIR_LOCK_INIT);  
os_unfair_lock_lock(unfairLock);  
os_unfair_lock_unlock(unfairLock);
os_unfair_lock_trylock(unfairLock);
复制代码

递归锁

需要使用递归锁的情况:

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveLock)(int);
    RecursiveLock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value = %d", value);
            sleep(2);
            RecursiveLock(value - 1);
        }
        [lock unlock];
    };
    RecursiveLock(5);
});
复制代码

这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所有每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所有它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。导致crach

*** -[NSLock lock]: deadlock ( '(null)')   *** Break on _NSLockError() to debug.
复制代码

NSRecursiveLock

递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

一个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。 NSRecursiveLockNSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

主要操作:

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
[lock lockBeforeDate:date];
[lock tryLock];

另外,NSRecursiveLock还声明了一个name属性,如下:

@property(copy) NSString *name

我们可以使用这个字符串来标识一个锁。Cocoa也会使用这个name作为错误描述信息的一部分。

条件锁

NSCondition

封装了一个互斥锁和信号量,它把前者的 lock 以及后者的 wait / signal 统一到 NSCondition 对象中,是基于条件变量 pthread_cond_t 来实现的,和信号量相似,如果当前线程不满足条件,那么就会进入睡眠状态,等待其他线程释放锁或者释放信号之后,就会唤醒线程。

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

  • NSCondition 同样实现了 NSLocking 协议,所以它和 NSLock 一样,也有NSLocking协议的 lockunlock 方法,可以当做 NSLock 来使用解决线程同步问题,用法完全一样。
  • NSCondition 提供了 waitsignal ,和条件信号量类似。比如我们要监听array数组的个数,当array的个数大于0的时候就执行清空操作。思路是这样的,当array个数大于0时执行清空操作,否则, wait 等待执行清空操作。当array个数增加的时候发生 signal 信号,让等待的线程唤醒继续执行。
    NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //消费者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        [lock unlock];
    });
    //生产者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        [lock lock];
        [array addObject:@1];
        [lock signal];
        [lock unlock];
    });
    复制代码
  • NSCondition 可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。例如:

    不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.

NSCoditionLock

NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。

NSConditionLock同样实现了NSLocking协议,性能比较低。

NSConditonLock 内部持有了一个 NSCondition 对象和 _condition_value 属性,当调用

- (instancetype)initWithCondition:(NSInteger)condition

初始化的时候会传入一个 condition 参数,该参数会赋值 _condition_value 属性

常用方法:

  • lock 不分条件,如果锁没被申请,直接执行代码
  • lockBeforeDate: 在指定时间前尝试加锁,返回 bool
  • lockWhenCondition: 满足特定条件 Condition ,加锁执行相应代码
  • lockWhenCondition: beforeDate: 和上条相同,增加时间戳
  • tryLock 尝试着加锁,返回 bool
  • tryLockWhenCondition: ,满足特定条件 Condition ,尝试着加锁,返回 bool
  • unlock 不会清空条件,之后满足条件的锁还会执行
  • unlockWithCondition: 设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)

实例:

- (void)executeNSConditionLock {
    NSConditionLock* lock = [[NSConditionLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (NSUInteger i=0; i<3; i++) {
            sleep(2);
            if (i == 2) {
                [lock lock];
                [lock unlockWithCondition:i];
            }
        }
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [self threadMethodOfNSCoditionLock:lock];
    });
}
-(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{
    [lock lockWhenCondition:2];
    [lock unlock];
}
复制代码

读写锁

读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。

pthread_rwlock

读写锁可以有三种状态:

  • 读模式下加锁状态,
  • 写模式下加锁状态,
  • 不加锁状态。

一次只有一个线程可以占有写模式的读写锁,但是多个线程可用同时占有读模式的读写锁。读写锁也叫做 共享-独占锁 ,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的。

因此:

  • 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行
  • 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。

注意:

  • 如果自己已经获取了读锁,再去加写锁,会出现死锁的
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//获取一个读出锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr); 
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
//获取一个写入锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr); 
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

int pthread_rwlock_unlock(pthread_rwlock_t *rwptr); //释放一个写入锁或者读出锁


//读写锁属性:
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr)
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);

int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int valptr);
复制代码

实例:

// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
// 读模式
pthread_rwlock_wrlock(&lock);
// 写模式
pthread_rwlock_rdlock(&lock);
// 读模式或者写模式的解锁
pthread_rwlock_unlock(&lock);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:1];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:2];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self writeBook:3];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self writeBook:4];
});
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self readBookWithTag:5];
});
- (void)readBookWithTag:(NSInteger )tag {
    pthread_rwlock_rdlock(&rwLock);
    self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"];
    self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil];
    pthread_rwlock_unlock(&rwLock);
}
- (void)writeBook:(NSInteger)tag {
    pthread_rwlock_wrlock(&rwLock);
    [self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    pthread_rwlock_unlock(&rwLock);
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Computer Age Statistical Inference

Computer Age Statistical Inference

Bradley Efron、Trevor Hastie / Cambridge University Press / 2016-7-21 / USD 74.99

The twenty-first century has seen a breathtaking expansion of statistical methodology, both in scope and in influence. 'Big data', 'data science', and 'machine learning' have become familiar terms in ......一起来看看 《Computer Age Statistical Inference》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具