iOS多线程

iOS 中的多线程分为以下四种:pthread、 NSThread、GCD、NSOperation/NSOperationQueue。 其中 NSOperation/NSOperationQueue 是对 GCD 面向对象的封装。

基本概念

线程 VS. 进程

  • 进程(Process):指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了

  • 线程(thread):操作系统进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.一个进程中至少包含一条线程,即主线程.

同步 VS. 异步

  • 同步: 同步操作会等待操作执行完成后,再继续执行接下来的代码(会堵塞当前线程),同步即在当前线程执行,没有开辟新线程的能力。

  • 异步: 异步操作在调用后立即返回,不会等待操作的执行结果(不会堵塞当前线程),异步操作有开辟新线程的能力。

串行 VS. 并发(concurrency) VS. 并行(parallelism)

从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。

并行(parallelism):单核心机器上通过时间片和上下文切换来实现多线程的并发(宏观意义上的同时执行);真正的多核心机器上,多个线程可以真正并行(真正的同时)执行。原理如图:

img

串行队列 VS. 并发队列

  • 队列: 队列 (queue),是先进先出(FIFO, First-In-First-Out)的线性表.

  • 串行队列: 串行队列是指队列中的任务是按照先后顺序一个接一个地执行的,队首的任务执行后才会执行其后的任务,直至执行到队尾的任务.

img

  • 并发队列: 并发队列是指队列中的任务可以同时地执行,即开始执行队首的任务后,不必等其执行完毕就可以接着开始执行队首之后的任务,因此在某一时刻可能存在同时执行的多个任务。先添加的任务的先执行。
    img

队列 VS. 线程

在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理。

iOS的并发编程模型

与直接创建线程的方式不同,我们只需定义好要调度的任务,然后让系统帮我们去执行这些任务就可以了。我们可以完全不需要关心线程的创建与销毁、以及多线程之间的同步等问题,苹果已经在系统层面帮我们处理好了,并且比我们手动地管理这些线程要高效得多。

NSThread

在 iOS 中使用 NSThread 对象建立一个线程非常方便,但是线程的生命周期需要程序员手动管理,也不便于管理多个线程.

两种显式调用

1.实例方法

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil];
[thread start];

2.detachNewThread类方法

[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];

隐式调用

NSObject实例方法,隐藏线程的存在

performSelectorInBackground:withObject:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelectorOnMainThread:withObject:waitUntilDone:

一些常用的方法

// 获得当前线程
+ (NSThread *)currentThread;
// 线程休眠
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 获取主线程
+ (NSThread *)mainThread;
// 判断当前线程是否主线程
- (BOOL)isMainThread;
+ (BOOL)isMainThread;
// 判断线程是否正在运行
- (BOOL)isExecuting;
// 判断线程是否已结束
- (BOOL)isFinished;

GCD

GCD 指的是 Grand Central Dispatch, 是 Apple 提供的一个多核编程的解决方案.GCD 是基于 C 语言的框架,可以充分利用多核,是 Apple 推荐使用的多线程技术.它不需要像 NSThread 一样需要手动管理线程的生命周期,它会被操作系统自动管理.

GCD 中的队列

串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
两个特殊的队列:
main 队列(串行队列)
dispatch_queue_t mainQueue = dispatch_get_main_queue();

和 NSThread 中的 + (NSThread *)mainThread; 一样,用于获取 main 队列, 即 当前进程的主线程.

在主队列里面执行同步执行任务会造成死锁现象 详情请看

全局队列(并发队列)

GCD 默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建.

// 默认权限的 全局队列
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
...
// 有以下几种优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

GCD 的同步/异步

在 默认权限下的全局队列 中执行 同步操作

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                // 同步执行的代码
        });

在 默认权限下的全局队列 中执行 异步操作

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 异步执行的代码
});

dispatch_sync

同步等待追加任务到队列

dispatch_async

异步非等待追加任务到队列

dispatch_set_target_queue

dispatch_set_target_queue()函数不仅可以设置queue的优先级,还可以设置queue之间的层级结构

设置优先级
dispatch_queue_t serialQueue = dispatch_queue_create("com.oukavip.www",NULL);  
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);  
dispatch_set_target_queue(serialQueue, globalQueue);

/ 第一个参数为要设置优先级的queue,第二个参数是参照物,既将第一个queue的优先级和第二个queue的优先级设置一样。
*/

设置queue之间的层级结构
+(void)testTargetQueue {
    dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);
    dispatch_set_target_queue(queue1, targetQueue);
    dispatch_set_target_queue(queue2, targetQueue);
    dispatch_set_target_queue(queue3, targetQueue);
    dispatch_async(queue1, ^{
        NSLog(@"1 in");
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"1 out");
    });
    dispatch_async(queue2, ^{
        NSLog(@"2 in");
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"2 out");
    });
    dispatch_async(queue3, ^{
        NSLog(@"3 in");
        [NSThread sleepForTimeInterval:1.f];
        NSLog(@"3 out");
    });
}

输出
 1 in
 1 out
 2 in
 2 out
 3 in
 3 out

总结:

通过打印的结果说明我们设置了queue1和queue2队列以targetQueue队列为参照对象,那么queue1和queue2中的任务将按照targetQueue的队列处理。

适用场景:

一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。这时候dispatch_set_target_queue将起到作用。

dispatch_after

dispatch_after 延迟一段时间把一项任务提交到队列中执行,这里需要注意的是: 延时的操作是提交,而不是运行.

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 延迟操作的代码

});

dispatch_group

我们使用 group 来管理一组异步代码,当它们都执行完后会调用同步或者异步(dispatch_group_notify)回调通知我们.

异步回调

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});
dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});
dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
   // 当所有异步执行的代码执行完毕会进入
});

同步回调

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});
dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});
dispatch_group_async(group, queue, ^{
   // 异步执行的代码
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

注: dispatch_group_wait 第二个参数表示超时时间. DISPATCH_TIME_FOREVER 表示没有超时时间.

dispatch_barrier_async

dispatch_barrier_async等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,这里指定的并发队列应该是通过dispatch_queue_create函数创建的。如果是一个串行队列或者全局并发队列,这个函数等同于dispatch_async函数。dispatch barrier 允许在一个并发队列中创建一个同步点,可以用于数据读写同步等。

dispatch_queue_t queue = dispatch_queue_create("com.cxy.gcdtest", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"dispatch_async1");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"dispatch_async2");
});
dispatch_barrier_async(queue, ^{
    [NSThread sleepForTimeInterval:4];
    NSLog(@"dispatch_barrier_async");

});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"dispatch_async3");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"dispatch_async4");
});
//由于并行队列,执行顺序不定
//    2016-11-25 11:45:47.571 GCD[2283:103879] dispatch_async2
//    2016-11-25 11:45:47.571 GCD[2283:103877] dispatch_async1
//    2016-11-25 11:45:51.575 GCD[2283:103877] dispatch_barrier_async
//    2016-11-25 11:45:52.580 GCD[2283:103877] dispatch_async3
//    2016-11-25 11:45:52.580 GCD[2283:103879] dispatch_async4

dispatch_apply

dispatch_apply 该函数按指定的次数将指定的block追加到指定的queue中,并等待全部处理执行结束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
   NSLog(@"%zu", index);
});
NSLog(@"任务完成");

输出结果:
2016-11-09 17:20:34.468 GCD[9067:868142] 0
2016-11-09 17:20:34.468 GCD[9067:868238] 2
2016-11-09 17:20:34.468 GCD[9067:868216] 1
2016-11-09 17:20:34.468 GCD[9067:868219] 3
2016-11-09 17:20:34.468 GCD[9067:868142] 4
2016-11-09 17:20:34.468 GCD[9067:868238] 5
2016-11-09 17:20:34.468 GCD[9067:868216] 6
2016-11-09 17:20:34.468 GCD[9067:868142] 8
2016-11-09 17:20:34.468 GCD[9067:868219] 7
2016-11-09 17:20:34.469 GCD[9067:868238] 9
2016-11-09 17:20:34.469 GCD[9067:868142] 任务完成

由于是全局的并行队列,任务的执行顺序不定,但由于要等待全部处理执行结束,因此”任务完成”总是在最后输出。这与dispatch_sync函数相同。

dispatch_suspend / dispatch_resume

dispatch_suspend 用于暂停\挂起队列,挂起后,追加到队列中但尚未执行的处理在此之后可以停止执行,但对已经正在执行的处理没有影响。

dispatch_resume 用于恢复队列,挂起后,追加到队列中但尚未执行的处理在此之后可以停止执行,恢复使这些处理能够继续执行。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 挂起指定的队列
dispatch_suspend(queue);
// 恢复指定队列
dispatch_resume(queue);

Dispatch Semaphore 信号量

利用信号量同步线程,过程更为精细。
比如下面的代码,不使用信号量的情况下会报pointer being freed was not allocated错误,这是由于在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错问题。

for (NSInteger i = 0; i < 10000; i ++) {
    dispatch_async(queue, ^{
        [arr addObject:[NSNumber numberWithInteger:i]];
    });
}

利用信号量可以使线程同步,避免访问错误

dispatch_semaphore_t sem = dispatch_semaphore_create(1);
for (NSInteger i = 0; i < 10000; i ++) {
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        //your code here
        [arr addObject:[NSNumber numberWithInteger:i]];
        dispatch_semaphore_signal(sem);
    });
}

在GCD中有三个函数是semaphore的操作,分别是:

dispatch_semaphore_create   创建一个semaphore

dispatch_semaphore_signal   发送一个信号

dispatch_semaphore_wait    等待信号

简单的介绍一下这三个函数,第一个函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量加1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1。

问题:如何创建一个并发数为10的一个并发队列?

dispatch_group_t group = dispatch_group_create();   
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);   
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);   
for (int i = 0; i < 100; i++)   {   
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);   
    dispatch_group_async(group, queue, ^{   
        NSLog(@"%i",i);   
        sleep(2);   
        dispatch_semaphore_signal(semaphore);   
    });   
}   
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);   
dispatch_release(group);   
dispatch_release(semaphore);   

创建了一个初使值为10的semaphore,每一次for循环都会创建一个新的线程,线程结束的时候会发送一个信号,线程创建之前会信号等待,所以当同时创建了10个线程之后,for循环就会阻塞,等待有线程结束之后会增加一个信号才继续执行,如此就形成了对并发的控制

dispatch_once

dispatch_once 保证在 App 运行期间,block 中的代码只执行一次.
一般用在实现单例模式上:

//单例模式
static id _instance;
+ (instancetype)sharedSingleton{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}
- (id)copyWithZone:(NSZone *)zone{
    return _instance;
}

Dispatch I/O

Dispatch I/O专门用于多线程下磁盘文件读写。在读取较大的文件时,如果将文件分成合适的大小并使用 Global Dispatch Queue 并列读取的话,应该会比一般的读取速度快不少,在 GCD 当中能实现这一功能的就是 Dispatch I/O 和 Dispatch Data。

Dispatch Source

参考:https://www.dreamingwish.com/article/grand-central-dispatch-basic-3.html

NSOperation/NSOperationQueue

NSOperation/NSOperationQueue 是 Apple 对 GCD 的封装,一个建立在 GCD 的基础之上的,面向对象的解决方案。相对 GCD 来说,使用 NSOperation/NSOperationQueue 会增加一点点额外的开销,与此同时也也获得了更好的灵活性。我们可以指定各个 NSOperation 之间的依赖关系,也可以继承 NSOperation 实现可复用的逻辑模块。

NSOperation

NSOperation 用来表示一个需要执行的任务。NSOperation 本身是一个抽象类,不能直接实例化,但是系统提供了两个子类用于封装任务,分别是: NSInvocationOperation 和 NSBlockOperation,同时也可以自定义子类。创建一个 NSOperation 对象后,需要调用 start 方法来启动任务,需要注意的是它会默认在当前队列同步执行。

NSInvocationOperation

通过一个 object 和 selector 我们可以非常方便地创建一个 NSInvocationOperation。

NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
[operation start];
NSBlockOperation

我们可以使用 NSBlockOperation 来并发执行一个或多个 block。可以通过NSBlockOperation的实例方法 addExecutionBlock 实现类似 GCD 中的 dispatch_group 的功能。

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
   // do something
}];
[operation start];

使用有 NSOperation 有什么灵活性呢?

1.添加依赖关系

NSOperation 有一个非常实用的功能,那就是添加依赖。它可以让一个 operation 只有在它依赖的所有 operation 都执行完成后才能开始执行。

- (void)addDependency:(NSOperation *)op;

2.队列中的优先级

对于被添加到 queue 中的 operation 来说,在 isReady(取决于依赖关系) 状态下,决定它们执行顺序的是它们在队列中的优先级。默认情况下都是 normal 的,但是我们可以根据需要通过 setQueuePriority: 方法来提高或降低 operation 的队列优先级。有以下几种优先级给我们选择:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

3.可以在任务执行完进行回调

[operation1 setCompletionBlock:^{
    // do something  
}];

NSOperationQueue

NSOperationQueue 并不像 GCD 中的队列那么复杂,默认都是并行的。通过设置 NSOperationQueue 的 maxConcurrentOperationCount 属性为1,那么它就变成了串行队列。NSOperationQueue 提供了获取 main 队列的方法:

+ (NSOperationQueue *)mainQueue NS_AVAILABLE(10_6, 4_0);
不过一般情况下我们会使用

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

进行初始化创建,然后通过以下三种方法添加任务,如果使用了下面三个方法,那么自动执行了 NSOperation 的 start 方法

// 添加一个NSOperation
- (void)addOperation:(NSOperation *)op;
// 添加一组NSOperation
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);
// 以 block 的形式 添加任务(代替 NSBlockOperation)
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);

常见问题总结

在串行队列中进行异步操作,会发生什么?

如果加入的串行队列是手动创建的,那么会另开线程执行,从而不会堵塞当前线程。
如果加入的是当前线程的串行队列,那么会堵塞当前线程。比如: 将异步操作放到 main 队列(通过 GCD 的 dispatch_get_main_queue() 获取)。

在并行队列中进行同步操作,会发生什么?

直接堵塞当前线程,直至同步操作执行完。

内存管理

…..

参考

http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/

Grand Central Dispatch Tutorial for Swift 4