线程安全与锁

NSInteger total = 0;
- (void)threadNotSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
        });
    }
}
//第一次输出:
2017-11-28 23:34:11.551570+0800 BasicDemo[75679:5312246] total: 1
2017-11-28 23:34:11.551619+0800 BasicDemo[75679:5312248] total: 3
2017-11-28 23:34:11.551618+0800 BasicDemo[75679:5312249] total: 2
2017-11-28 23:34:11.552120+0800 BasicDemo[75679:5312246] total: 2
2017-11-28 23:34:11.552143+0800 BasicDemo[75679:5312248] total: 1
2017-11-28 23:34:11.552171+0800 BasicDemo[75679:5312249] total: 0
//第二次输出
2017-11-28 23:34:55.738947+0800 BasicDemo[75683:5313401] total: 1
2017-11-28 23:34:55.738979+0800 BasicDemo[75683:5313403] total: 2
2017-11-28 23:34:55.738985+0800 BasicDemo[75683:5313402] total: 3
2017-11-28 23:34:55.739565+0800 BasicDemo[75683:5313401] total: 2
2017-11-28 23:34:55.739570+0800 BasicDemo[75683:5313402] total: 1
2017-11-28 23:34:55.739577+0800 BasicDemo[75683:5313403] total: 0
NSInteger total = 0;
NSLock *lock = [NSLock new];
- (void)threadSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
            [lock unlock];
        });
    }
}
//第一次输出
2017-11-28 23:35:37.696614+0800 BasicDemo[75696:5314483] total: 1
2017-11-28 23:35:37.696928+0800 BasicDemo[75696:5314483] total: 0
2017-11-28 23:35:37.696971+0800 BasicDemo[75696:5314481] total: 1
2017-11-28 23:35:37.696995+0800 BasicDemo[75696:5314481] total: 0
2017-11-28 23:35:37.697026+0800 BasicDemo[75696:5314482] total: 1
2017-11-28 23:35:37.697050+0800 BasicDemo[75696:5314482] total: 0
//第二次输出
2017-11-28 23:36:01.790264+0800 BasicDemo[75700:5315159] total: 1
2017-11-28 23:36:01.790617+0800 BasicDemo[75700:5315159] total: 0
2017-11-28 23:36:01.790668+0800 BasicDemo[75700:5315161] total: 1
2017-11-28 23:36:01.790687+0800 BasicDemo[75700:5315161] total: 0
2017-11-28 23:36:01.790711+0800 BasicDemo[75700:5315160] total: 1
2017-11-28 23:36:01.790735+0800 BasicDemo[75700:5315160] total: 0

从上面可以看出,第一个函数第一次和第二次调用的结果不一样,换句话说,不能确定代码的运行顺序和结果,是线程不安全的;第二个函数第一次和第二次输出结果一样,可以确定函数的执行结果,是线程安全的。

线程不安全是由于多线程访问造成的,那么如何解决?

 

1.既然线程安全问题是由多线程引起的,那么,最极端的可以使用单线程保证线程安全。

2.线程安全是由于多线程访问和修改共享资源而引起不可预测的结果,因此,如果都是访问共享资源而不去修改共享资源也可以保证线程安全,比如:设置只读属性的全局变量。

3.使用锁。

  锁是最常用的同步工具。一段代码段在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。

 
NSLock
在Cocoa程序中NSLock中实现了一个简单的互斥锁,实现了NSLocking protocol。
lock,加锁
unlock,解锁
tryLock,尝试加锁,如果失败了,并不会阻塞线程,只是立即返回
NOlockBeforeDate:,在指定的date之前暂时阻塞线程(如果没有获取锁的话),如果到期还没有获取锁,则线程被唤醒,函数立即返回NO
使用tryLock并不能成功加锁,如果获取锁失败就不会执行加锁代码了。
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

@synchronized代码块

每个iOS开发最早接触的线程锁就是@synchronized,代码简单。

- (void)getIamgeName:(int)index{
    NSString *imageName;
    @synchronized(self) {
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
    }
}

条件信号量dispatch_semaphore_t

dispatch_semaphore_tGCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。

#import "DispatchSemaphoreViewController.h"

@interface DispatchSemaphoreViewController ()
{
    dispatch_semaphore_t semaphore;
}
@end

@implementation DispatchSemaphoreViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    semaphore = dispatch_semaphore_create(1);
    /**
     *  创建一个信号量为1的信号
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  semaphore:等待信号
     DISPATCH_TIME_FOREVER:等待时间
     wait之后信号量-1,为0
     */
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    /**
     *  发送一个信号通知,这时候信号量+1,为1
     */
    dispatch_semaphore_signal(semaphore);
}

@end

 

条件锁NSCondition

NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lock和unlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}
  同时,NSCondition提供更高级的用法。wait和signal,和条件信号量类似。
  比如我们要监听imageNames数组的个数,当imageNames的个数大于0的时候就执行清空操作。思路是这样的,当imageNames个数大于0时执行清空操作,否则,wait等待执行清空操作。当imageNames个数增加的时候发生signal信号,让等待的线程唤醒继续执行。
  NSCondition和NSLock、@synchronized等是不同的是,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.
当我用数组做删除试验时,做增删操作并不是每次都会出现,大概3-4次后会出现。单纯的使用lock、unlock是没有问题的。
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];    //加锁
    static int m = 0;
    static int n = 0;
    static int p = 0;
    NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);
    
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        m++;
        NSLog(@"执行了%d次删除操作",m);
    } else {
        p++;
        NSLog(@"执行了%d次等待",p);
        [lock wait];    //等待
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        /**
         *  有时候点击取出图片会崩溃
         */
        n++;
        NSLog(@"执行了%d次继续操作",n);
    }
    
    NSLog(@"removeObject count: %ld\n",imageNames.count);
    [lock unlock];     //解锁
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lock];
    static int m = 0;
    [imageNames addObject:@"0"];
    m++;
    NSLog(@"添加了%d次",m);
    [lock signal];  //唤醒随机一个线程取消等待继续执行
    
//        [lock broadcast];   //唤醒所有线程取消等待继续执行
    NSLog(@"createImageName count: %ld\n",imageNames.count);
    [lock unlock];
}

#pragma mark - 多线程取出图片后删除
- (void)getImageNameWithMultiThread{
    [lock broadcast];
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
    
}

条件锁NSConditionLock

也有人说这是个互斥锁
NSConditionLock同样实现了NSLocking协议,试验过程中发现性能很低。

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

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

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lockWhenCondition:1];    //加锁
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
    }
    [lock unlockWithCondition:0];     //解锁
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lockWhenCondition:0];
    [imageNames addObject:@"0"];
    [lock unlockWithCondition:1];
}

#pragma mark - 多线程取出图片后删除
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10000; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
}

递归锁NSRecursiveLock

  有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
        [self getIamgeName:imageNames];
    }
    [lock unlock];
}
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*10;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
        [self getIamgeName:imageNames];
    });
    dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
    
}

NSDistributedLock

  NSDistributedLock是MAC开发中的跨进程的分布式锁,底层是用文件系统实现的互斥锁。NSDistributedLock没有实现NSLocking协议,所以没有lock方法,取而代之的是非阻塞的tryLock方法。

NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
    while (![lock tryLock])
    {
        sleep(1);
    }
    
    //do something
    [lock unlock];
当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.其他应用也不能访问受保护的共享资源。在这种情况下,你可以使用breadLock方法来打破现存的锁以便你可以获取它。但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。

互斥锁POSIX

POSIX和dispatch_semaphore_t很像,但是完全不同。POSIX是Unix/Linux平台上提供的一套条件互斥锁的API。
新建一个简单的POSIX互斥锁,引入头文件#import <pthread.h>声明并初始化一个pthread_mutex_t的结构。使用pthread_mutex_lock和pthread_mutex_unlock函数。调用pthread_mutex_destroy来释放该锁的数据结构。
#import <pthread.h>
@interface MYPOSIXViewController ()
{
    pthread_mutex_t mutex;  //声明pthread_mutex_t的结构
}
@end

@implementation MYPOSIXViewController
- (void)dealloc{
    pthread_mutex_destroy(&mutex);  //释放该锁的数据结构
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    pthread_mutex_init(&mutex, NULL);
    /**
     *  初始化
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  加锁
     */
    pthread_mutex_lock(&mutex);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  解锁
     */
    pthread_mutex_unlock(&mutex);
}

POSIX还可以创建条件锁,提供了和NSCondition一样的条件控制,初始化互斥锁同时使用pthread_cond_init来初始化条件数据结构,

// 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    
    // 等待(会阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    
    // 定时等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    
    // 唤醒
    int pthread_cond_signal (pthread_cond_t *cond);
    
    // 广播唤醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    
    // 销毁
    int pthread_cond_destroy (pthread_cond_t *cond);
OSIX还提供了很多函数,有一套完整的API,包含Pthreads线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS的,是基于Linux的,但是介绍的非常详细 Linux 线程锁详解
 

自旋锁OSSpinLock

首先要提的是OSSpinLock已经出现了BUG,导致并不能完全保证是线程安全的。

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。
所以说不建议再继续使用,不过可以拿来玩耍一下,导入头文件#import <libkern/OSAtomic.h>
#import <libkern/OSAtomic.h>
@interface MYOSSpinLockViewController ()
{
    OSSpinLock spinlock;  //声明pthread_mutex_t的结构
}
@end

@implementation MYOSSpinLockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    spinlock = OS_SPINLOCK_INIT;
    /**
     *  初始化
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  加锁
     */
    OSSpinLockLock(&spinlock);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  解锁
     */
    OSSpinLockUnlock(&spinlock);
}
@end

 

GCD线程阻断dispatch_barrier_async/dispatch_barrier_sync

dispatch_barrier_async/dispatch_barrier_sync在一定的基础上也可以做线程同步,会在线程队列中打断其他线程执行当前任务,也就是说只有用在并发的线程队列中才会有效,因为串行队列本来就是一个一个的执行的,你打断执行一个和插入一个是一样的效果。两个的区别是是否等待任务执行完成。

注意:如果在当前线程调用dispatch_barrier_sync打断会发生死锁。

@interface MYdispatch_barrier_syncViewController ()
{
        __block double then, now;
}
@end

@implementation MYdispatch_barrier_syncViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }else{
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    }
}

- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*11;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<count+1; i++) {
        //100来测试锁有没有正确的执行
        dispatch_barrier_async(self.synchronizationQueue, ^{
             [self getIamgeName:imageNames];
        });
    }
}

总结

@synchronized:适用线程不多,任务量不大的多线程加锁
NSLock:其实NSLock并没有想象中的那么差,不知道大家为什么不推荐使用
dispatch_semaphore_t:使用信号来做加锁,性能提升显著
NSCondition:使用其做多线程之间的通信调用不是线程安全的
NSConditionLock:单纯加锁性能非常低,比NSLock低很多,但是可以用来做多线程处理不同任务的通信调用
NSRecursiveLock:递归锁的性能出奇的高,但是只能作为递归使用,所以限制了使用场景
NSDistributedLock:因为是MAC开发的,就不讨论了
POSIX(pthread_mutex):底层的api,复杂的多线程处理建议使用,并且可以封装自己的多线程
OSSpinLock:性能也非常高,可惜出现了线程问题
dispatch_barrier_async/dispatch_barrier_sync:测试中发现dispatch_barrier_sync比dispatch_barrier_async性能要高,真是大出意外








 

posted @ 2018-01-02 16:04  FMDN  阅读(750)  评论(0编辑  收藏  举报