一.iOS中的并发编程

iOS中的多线程基于Cocoa框架实现,Cocoa 中封装了 NSThread, NSOperation, GCD 三种多线程编程方式:

  • NSThread

    NSThread 是一个控制线程执行的对象,通过它我们可以方便的得到一个线程并控制它。NSThread 的线程之间的并发控制,是需要我们自己来控制的,可以通过 NSCondition 实现。它的缺点是需要自己维护线程的生命周期和线程的同步和互斥等,优点是轻量,灵活。

  • NSOperation

    NSOperation 是一个抽象类,它封装了线程的细节实现,不需要自己管理线程的生命周期和线程的同步和互斥等。只是需要关注自己的业务逻辑处理,需要和 NSOperationQueue 一起使用。使用 NSOperation 时,你可以很方便的设置线程之间的依赖关系。这在略微复杂的业务需求中尤为重要。

  • GCD

    GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。在 iOS4.0 开始之后才能使用。GCD 是一个可以替代 NSThread 的很高效和强大的技术。当实现简单的需求时,GCD 是一个不错的选择。

NSThread是系统底层C语言Pthread的面向对象API实现,Grand Central Dispatch(GCD) 是苹果实现的一套高性能并发编程机制底层由libdispatch库实现,NSOperationQueue基于 GCD 的更高层的封装支持更高级的任务控制。在多线程编程开发工作中常使用GCD以及NSOperation。

二.线程同步

假设两个异步并发线程(它们执行顺序是随机的)在访问同一个资源的时候,就会发生资源竞争的问题。那么我们就需要通过一些手段来保护这个资源,让多线程可以有序访问这个资源而不产生问题,这就是线程同步。

在iOS中针对这个问题实现的几套常用的技术:

  • 语法层线程同步:@synchronized{}(互斥锁)
  • NS系列线程同步:NSLock(互斥锁),NSRecursiveLock(递归锁),NSCondition(条件锁)
  • GCD中的线程同步:dispatch_semaphore(信号量)
  • OS中的线程同步:pthread_mutex,OSSpinLock(自旋锁)

@synchronized

NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(obj) {
         NSLog(@"需要线程同步的操作1 开始");
         sleep(3); 
         NSLog(@"需要线程同步的操作1 结束");
     } 
  }); 

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    sleep(1); 
    @synchronized(obj) { 
         NSLog(@"需要线程同步的操作2"); 
     } 
});

@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,两个异步线程才为满足互斥。@synchronized指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。

NSLock:互斥锁

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    //[lock lock]; 
    [lock lockBeforeDate:[NSDate date]];
    NSLog(@"需要线程同步的操作1 开始"); 
    sleep(2); 
    NSLog(@"需要线程同步的操作1 结束"); 
    [lock unlock];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    if ([lock tryLock]) {
        //尝试获取锁,如果获取不到返回NO,不会阻塞该线程 
        NSLog(@"锁可用的操作"); 
        [lock unlock]; 
    }else{
        NSLog(@"锁不可用的操作"); 
    } 

    NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3]; 
    if ([lock lockBeforeDate:date]) {
          //尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
          NSLog(@"没有超时,获得锁"); 
          [lock unlock]; 
     }else{ 
          NSLog(@"超时,没有获得锁"); 
     } 
});

// 死锁
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    static void (^block)(int); 
    block = ^(int value) { 
        [lock lock];
        if (value > 0) { 
            NSLog(@"value = %d", value); 
            sleep(1); 
            block(value - 1);// 递归
        } 
        [lock unlock]; 
    };

    block(5);
});

NSLock是NS系列实现的基本锁对象,我们在使用lock方法保护线程中的资源,当另一线程访问该资源的时候会让它陷入阻塞等待unlock释放出该资源才能使用。同时它提供trylock方法来检测资源是否被锁住了。当我们在某些时候(如锁内递归调用)持有锁的对象再次持有一次(对资源多次加锁)就会陷入程序死锁。

NSRecursiveLock:递归锁

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    static void (^RecursiveBlock)(int); 
    RecursiveBlock= ^(int value) { 
        [lock lock]; 
        if (value > 0) { 
            NSLog(@"value = %d", value); 
            sleep(1); 
            RecursiveBlock(value - 1); 
        } 
        [lock unlock]; 
    };
    RecursiveBlock(5); 
});

在这种情况下,我们就可以使用NSRecursiveLock,它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作,只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

NSConditionLock与NSCondition:条件锁

// NSConditionLock
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSMutableArray *products = [NSMutableArray array]; 
NSInteger HAS_DATA = 1; 
NSInteger NO_DATA = 0;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    while (1) { 
        [lock lockWhenCondition:NO_DATA]; 
        [products addObject:[[NSObject alloc] init]]; 
        NSLog(@"produce a product,总量:%zi",products.count); 
        [lock unlockWithCondition:HAS_DATA]; 
        sleep(1); 
   } 
}); 

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
    while (1) { 
         NSLog(@"wait for product"); 
         [lock lockWhenCondition:HAS_DATA]; 
         [products removeObjectAtIndex:0]; 
         NSLog(@"custome a product"); 
         [lock unlockWithCondition:NO_DATA]; 
    } 
});


// NSCondition
NSCondition *condition = [[NSCondition alloc] init]; 
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    while (1) { 
        [condition lock]; 
        if ([products count] == 0) { 
            NSLog(@"wait for product"); 
           [condition wait]; 
       }
      [products removeObjectAtIndex:0];
      NSLog(@"custome a product"); 
      [condition unlock]; 
     } 
}); 

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
     while (1) { 
        [condition lock]; 
        [products addObject:[[NSObject alloc] init]]; 
        NSLog(@"produce a product,总量:%zi",products.count); 
        [condition signal]; 
        [condition unlock]; 
        sleep(1); 
    } 
});

前面几种锁可以解决资源竞争的基本问题,但如果要实现一些复杂的问题如经典的生产-消费问题实现上就比较麻烦或者实现效率不高。这里我们可以通过条件锁来提供一个开锁条件来更加优雅解决这些问题,NSConditionLock和NSCondition的区别在与NSCondition能够手动管理开锁条件。

dispatch_semaphore:GCD内的信号量

dispatch_semaphore_t signal = dispatch_semaphore_create(1); 
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(signal, overTime);// 等待超时3s
        NSLog(@"需要线程同步的操作1 开始"); 
        sleep(2);                                 // 睡眠2s
        NSLog(@"需要线程同步的操作1 结束");
        dispatch_semaphore_signal(signal); 
}); 

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 
        sleep(1);                                  // 睡眠1s
        dispatch_semaphore_wait(signal, overTime); // 等待超时3s
        NSLog(@"需要线程同步的操作2"); 
        dispatch_semaphore_signal(signal); 
});

信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。它是另外一种解决资源竞争的思路,表示出这个资源的资源数然后让线程们去竞争这些资源而未拿到这些资源的线程就陷入阻塞等待资源丰富后再去竞争这个资源。在资源为1的情况下它实现等同与NSLock的效果,在资源数丰富的情况下它能够更加高效处理这些资源。

锁的性能

对以上各个锁进行1000000次的加锁解锁的空操作时间如下:

方法 1000000消耗时间
OSSpinLock 46.15 ms
dispatch_semaphore 56.50 ms
pthread_mutex 178.28 ms
NSCondition 193.38 ms
NSLock 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock 157.44 ms
NSConditionLock: 490.04 ms
@synchronized 371.17 ms

OSSpinLock和dispatch_semaphore的效率远远高于其他的锁机制,但是鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。不考虑性能的话,@synchronized语法简练,而NSRecursiveLock可以避免死锁性能也不错也推荐使用。

三.GCD(Grand Central Dispatch)

GCD的由两个核心组成:任务(在闭包中所要执行的代码)和队列。同时还有几个重要的概念:

  • Dispatch Queue:Dispatch Queue 顾名思义,是一个用于维护任务的队列,它可以接受任务(即可以将一个任务加入某个队列)然后在适当的时候执行队列中的任务。
  • Dispatch Sources:Dispatch Source 允许我们把任务注册到系统事件上,例如 socket 和文件描述符,类似于 Linux 中 epoll 的作用。
  • Dispatch Groups:Dispatch Groups 可以让我们把一系列任务加到一个组里,组中的每一个任务都要等待整个组的所有任务都结束之后才结束,类似 pthread_join 的功能。
  • Dispatch Semaphores:这个更加顾名思义,就是信号量,可以让我们实现更加复杂的并发控制,防止资源竞争。

队列(dispatch queue)

Dispatch Queue 是一个 FIFO(First In, First Out)队列,因此任务开始执行的顺序,就是你把它们放到 queue 中的顺序。GCD 中的队列有下面几种:

  1. Serial (串行队列) 任务会按照添加到 queue 中的顺序一个一个执行。串行队列在前一个任务执行之前,后一个任务是被阻塞的,可以利用这个特性来进行同步操作。当你创建多个Serial queue时,虽然内部的任务各自是同步,但Serial queue之间是并发执行。

  2. Concurrent(并行队列) ,也叫 global dispatch queue,可以并发地执行多个任务,但是任务开始的顺序仍然是按照被添加到队列中的顺序。具体任务执行的线程和任务执行的并发数,都是由 GCD 进行管理的。系统提供四种优先级的全局并行队列。

  3. Main Dispatch Queue(主队列) 是一个全局可见的串行队列,其中的任务会在主线程中执行。主队列通过与应用程序的 runloop 交互,把任务安插到 runloop 当中执行。因为主队列比较特殊,其中的任务确定会在主线程中执行,通常主队列会被用作同步的作用。

  4. User create queue(自定义队列),可以用dispatch_queue_create函数,函数有两个参数,第一个自定义的队列名,第二个参数是队列类型,默认NULL或者DISPATCH_QUEUE_SERIAL的是串行,参数为DISPATCH_QUEUE_CONCURRENT为并行队列。同时还可以通过dipatch_queue_attr_make_with_qos_classdispatch_set_target_queue方法设置队列的优先级。

// 自定义创建队列

//dipatch_queue_attr_make_with_qos_class
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);
dispatch_queue_t queue = dispatch_queue_create("com.qosqueue", attr);

//dispatch_set_target_queue
dispatch_queue_t queue = dispatch_queue_create("com.settargetqueue",NULL); //需要设置优先级的queue
dispatch_queue_t referQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); //参考优先级
dispatch_set_target_queue(queue, referQueue); // 设置queue和referQueue的优先级一样

队列的优先级和自定义队列

系统提供的并行队列对应的优先级为

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

优先级依次降低。优先级越高的队列中的任务会更早执行,其对应的QOS值为:

  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。

  • QOS_CLASS_USER_INITIATED:user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。

  • QOS_CLASS_UTILITY:utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。

  • QOS_CLASS_BACKGROUND:background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。

事实上,我们自己创建的队列,最终会把任务分配到系统提供的主队列和四个全局的并行队列上,这种操作叫做 Target queues

具体来说,我们创建的串行队列的 Target queue 就是系统的主队列,我们创建的并行队列的Target queue 默认是系统 default 优先级的全局并行队列。所有放在我们创建的队列中的任务,最终都会到Target queue中完成真正的执行。

虽然我们是无法直接控制系统的队列中的任务,但是通过自定义队列间接(任务均会映射到系统不同优先级的Target queue中)来实现任务的控制。

注意事项

  • 同步和异步添加任务,与队列是串行队列和并行队列没有关系。可以同步地给并行队列添加任务,也可以异步地给串行队列添加任务。同步和异步添加任务只影响是不是阻塞当前线程,和任务的串行或并行执行没有关系。

  • Dispatch Queue 本身是线程安全的,你可以从系统的任何一个线程给 queue 添加任务,不需要考虑加锁和同步问题。

  • 避免在任务中使用锁,如果使用锁的话可能会阻碍 queue 中其他 task 的运行。

添加任务方式与队列类型

GCD队列的任务添加方式和任务所在队列的属性组合上需要特别注意,不恰当的组合会造成线程的死锁。

  • 同步添加任务:dispatch_sync会阻塞式(当前线程中)目标队列末尾添加一个任务,直到任务执行完成返回才继续执行下一个任务。
  • 异步添加任务:dispatch_async会非阻塞式(开一个线程)给目标队列末尾添加一个任务,不许要等到任务返回。
  • 串行队列:队列中的任务有序执行,需要等到上一个任务执行完才会执行下一个任务。
  • 并行队列:队列中的任务无序执行,并发处理量根据系统状态,若有阻塞任务执行会导致队列休眠不执行。
第一种:在主线程中同步添加任务到主队列中
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3

上述代码中,主队列中的任务添加顺序如图所示,由于任务2阻塞了主队列和任务3造成了死锁使得最终输出只有任务1。

第二种:在主线程中同步添加任务到全局队列中
NSLog(@"1"); // 任务1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"2"); // 任务2
});
NSLog(@"3"); // 任务3

上述代码的执行顺序为任务1,任务2,任务3。

第三种:在同一个串行队列中组合添加任务
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任务2
    dispatch_sync(queue, ^{
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5

上述代码中只有一个串行队列,任务的执行顺序为任务1,任务2/任务5,任务3和任务4死锁。

第四种:在主队列和全局并行队列组合添加任务
NSLog(@"1"); // 任务1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2"); // 任务2
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5

上述代码输出1,2/5,3,4。

如果想要设置线程间的依赖关系,那就需要嵌套,如果嵌套就会导致一些复杂的事情发生(可能发生死锁的问题)。这应该是 GCD 的一个非常明显的缺陷之一。

四.NSOperation与NSOperationQueue

NSOperation 本身是可以单独使用的,不过单独使用的话并不能体现出 NSOperation 的强大之处,通常还是使用 NSOperationQueue 来执行 NSOperation。对于复杂的线程依赖关系,虽然GCD也能办到但很麻烦,相反NSOperationQueue相对简单易用。

NSOperation独立运行

默认情况下 NSOperation 是阻塞运行的,相当于一个普通的函数调用:

@implementation MyOperation
// 方法重写
-(void)main {
    NSLog(@"MyOperation Main Function");
}
@end

#import "MyOperation.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyOperation *op = [[MyOperation alloc] init];
        [op start];
        NSLog(@"Main Function");
    }
    return 0;
}

输出:
MyOperation Main Function
Main Function

让NSOperation“并行”调用:

// MyOperation.h
#import <Foundation/Foundation.h>
@interface MyOperation : NSOperation
{
    BOOL        executing;
    BOOL        finished;
}
- (void)completeOperation;
@end

// MyOperation.m
#import "MyOperation.h"
@implementation MyOperation
- (id)init {
    self = [super init];
    if (self) {
        executing = NO;
        finished = NO;
    }
    return self;
}

- (void)start {
    // Always check for cancellation before launching the task.
    if ([self isCancelled])
    {
        // Must move the operation to the finished state if it is canceled.
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    // If the operation is not canceled, begin executing the task.
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
    @try {
        // Do the main work of the operation here.
        [self completeOperation];
    }
    @catch(...) {
        // Do not rethrow exceptions.
    }
}

- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    executing = NO;
    finished = YES;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return executing;
}

- (BOOL)isFinished {
    return finished;
}
@end

在NSOperationQueue中运行NSOperation

把一个 NSOperation 放到 NSOperationQueue 中,Queue 会忽略 isAsynchronous变量,总是会把 operation 放到后台线程中执行。使用 NSOperationQueue 可以很方便地进行并发操作,并且帮我们完成大部分的监视 operation 是否完成的操作。只要operation 正确地重载了 isExecuting 和 isFinished,就可以正确地被并发执行。

默认来说,NSOperationQueue内部的NSOperation 是并发执行的,你不能改变它的类型到串行队列,但你可以通过给任务添加依赖来实现串行队列。

Operaiton Dependency(可依赖)

依赖是一种方便的方式来改变队列任务执行的顺序。可以通过addDependency:removeDependency:方法来添加和删除操作依赖。默认情况下,Operation 对象被置为ready直到它所有的依赖完成执行。一旦最后一个依赖执行完成,操作对象会准备就绪,开始执行。

NSOperation的依赖没有区分一个依赖操作是否成功完成(比如 cancel 了一个操作,也被标记为这个操作已经完成)。操作A依赖操作B,如果想等到B成功执行后再来执行 A ,那就需要额外的代码来实现这一功能。

Cancellation(可取消)

NSOperation 有几种的运行状态:PendingReadyExecutingFinishedCanceled,除 Finished状态外,其他状态均可转换为 Canceled状态。

maxConcurrentOperationCount(最大并发数)

默认的最大并发 operation 数量是由系统当前的运行情况决定,我们也可以强制指定一个固定的并发数量。

Queue Priority(执行优先级)
typedef enum : NSInteger {
   NSOperationQueuePriorityVeryLow = -8,
   NSOperationQueuePriorityLow = -4,
   NSOperationQueuePriorityNormal = 0,
   NSOperationQueuePriorityHigh = 4,
   NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;

队列执行优先级表示是不同队列之间的执行优先级,尽管系统会尽量使得优先级高的任务优先执行,不过并不能确保优先级高的任务一定会先于优先级低的任务执行,即优先级并不能保证任务的执行先后顺序。要先让一个任务先于另一个任务执行,需要使用设置dependency 来实现。

四.GCD和NSOperation的区别

  • GCD是底层的C语言构成的API,而NSOperationQueue以及相关对象是基于GCD的Objective-C对象的封装。

  • NSOperation支持KVO(Key-Value Observing),可以方便的监听任务的状态(完成、执行中、取消等等状态)。

  • NSOperation可以用任务依赖设置同一个队列中任务的执行顺序,能够使同一个并行队列中的任务区分先后地执行,而在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码。

results matching ""

    No results matching ""