GCD(二)

说说GCD中的死锁和线程安全。

一、什么是死锁?

停止等待事情的线程会导致多个线程相互维持等待,即死锁。

多个线程卡住,并互相等待对方完成或执行其它操作;第一个不能完成是因为它在等待第二个的完成;但第二个也不能完成,因为它在等待第一个的完成。

二、常见的几种死锁示例

示例1

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });

    NSLog(@"2");
}

程序运行后会发现,会发现没有任何打印,直接就发生了死锁。

更改如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
    });

    NSLog(@"2");
}

总结:禁止在主队列(iOS开发中,主队列是串行队列)中,再同步使用主队列执行任务。

示例2

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("com.xiaohui.test", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{

        NSLog(@"1");

        dispatch_sync(queue, ^{
            NSLog(@"2");
        });

        NSLog(@"3");
    });
}

程序运行后会发现,只打印了1,然后就发生了死锁。

更改如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("com.xiaohui.test", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{

        NSLog(@"1");

        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2");
        });

        NSLog(@"3");
    });
}

总结:禁止在同一个同步(或异步)串行队列中,再使用该串行队列同步的执行任务。

示例3

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_queue_t queue = dispatch_queue_create("com.xiaohui.test", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{

        NSLog(@"1");

     //回到主线程发现死循环后面就没法执行了
        dispatch_sync(queue, ^{
            NSLog(@"2");
        });

        NSLog(@"3");
    });
}

//死循环
while (1) {
    //do something
}

程序运行后会发现,只打印了4,1,然后程序卡住了,发生了死锁。

去掉死循环语句即可。

三、线程安全

在访问数据库或者文件的时候,我们可以使用Serial Dispatch Queue可避免数据竞争问题,代码如下所示:
先看看,如果我们在平常编码中,如果要保证某个属性可以线程安全的读写,是如何写的:

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

#import "Person.h"

static NSString *_name;

@implementation Person

- (void)setName:(NSString *)name {
    @synchronized(self) {
        _name = [name copy];
    }
}

- (NSString *)name {
    @synchronized(self) {
        return _name;
        }
}

@end

这是我在刚学iOS开发,刚涉及并发中的数据竞争时,书本上提到的一种解决方案。如果有多个线程要执行同一份代码,那么有时候可能会出现问题,这种情况下,通常要使用锁来实现某种同步机制。iOS提供了一种加锁的方式,就是采用内置的synchronization block,也就是上面代码所写的。

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁也就释放了。在上面的例子中,同步行为所针对的对象是self。这么写通常没错,但是@synchronized(self)会大大降低代码效率,甚至很多时候,还可以被人感觉到效率明显下降了,因为共用同一个锁的那些同步块,都必须按顺序执行。若在self对象上频繁加锁,那么程序可能就要等另一段与此无关的代码执行完毕,才可以继续执行当前代码,这样做是很没必要的。

@synchronized(self)会大大降低代码效率,因为所有的同步块( @synchronized(self) )都会彼此抢夺同一个锁。要是有多个属性这么写,每个属性的同步块( @synchronized(self) )都要等其他所有的同步块执行完毕之后才能执行,这并不是我们想要的结果,我们只想要每个属性各自独立的同步。

还有,不得不说,按上面这么做,虽然可以在一定程度上提供“线程安全”,但却无法保证访问该对象时是绝对线程安全的。事实上,上面的写法,就是atomic,也就是原子性属性xcode自动生成的代码,这种方法,在访问属性时,必定可以从中得到有效值,然而如果在一个线程上多次调用getter方法,每次得到的结果却未必相同,在两次读操作之间,其他线程可能会写入新的属性值。

其实使用GCD可以简单高效的代替同步块或者锁对象,可以使用,串行同步队列,将读操作以及写操作都安排在同一个队列里,即可保证数据同步,代码如下:

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

#import "Person.h"

static NSString *_name;
static dispatch_queue_t _queue;

@implementation Person

- (instancetype)init {
    if (self = [super init]) {
       _queue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)setName:(NSString *)name {

    dispatch_sync(_queue, ^{

        _name = [name copy];
    });
}

- (NSString *)name {

    __block NSString *tempName;

    dispatch_sync(_queue, ^{

        tempName = _name;
    });
    return tempName;
}    

@end

这样写的思路是:把写操作与读操作都安排在同一个同步串行队列里面执行,这样的话,所有针对属性的访问操作就都同步了。
这种方法的确已经足够好了,但还不是最优的,它只可以实现单读、单写。整体来看,我们最终要解决的问题是,在写的过程中不能被读,以免数据不对,但是读与读之间并没有任何的冲突!

多个getter方法(也就是读取)是可以并发执行的,而getter(读)与setter(写)方法是不能并发执行的,利用这个特点,还能写出更快的代码来,这次注意,不用串行队列,而改用并行队列:

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

#import "Person.h"

static NSString *_name;
static dispatch_queue_t _concurrentQueue;

@implementation Person

- (instancetype)init {
    if (self = [super init]) {
       _concurrentQueue = dispatch_queue_create("com.person.syncQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)setName:(NSString *)name {
    dispatch_barrier_async(_concurrentQueue, ^{
        _name = [name copy];
    });
}

- (NSString *)name {
    __block NSString *tempName;
    dispatch_sync(_concurrentQueue, ^{
        tempName = _name;
    });
    return tempName;
}

@end

这样优化,测试一下性能,可以发现这种做法肯定比使用串行队列要快。

在这个代码中,我用了点新的东西,dispatch_barrier_async,可以翻译成栅栏(barrier),它可以往队列里面发送任务(块,也就是block),这个任务有栅栏(barrier)的作用。

在队列中,barrier块必须单独执行,不能与其他block并行。这只对并发队列有意义,并发队列如果发现接下来要执行的block是个barrier block,那么就一直要等到当前所有并发的block都执行完毕,才会单独执行这个barrier block代码块,等到这个barrier block执行完毕,再继续正常处理其他并发block。在上面的代码中,setter方法中使用了barrier block以后,对象的读取操作依然是可以并发执行的,但是写入操作就必须单独执行了。