iOS 多线程的实现方式及应用示例

93 阅读9分钟

 概述

优点:

  • 把程序中耗时的任务放到后台去处理,如图片、视频的下载等;
  • 充分发挥多核处理器的优势,并发执行让系统运行的更快、更流畅、用户体验更佳。

不足:

  • 大量的线程操作会降低代码的可读性;
  • 大量的线程需要更多的内存空间;
  • 当多个线程对同一资源出现争夺的时候会出现线程安全问题。

目前实现多线程的技术有四种:pthreadNSThreadGCDNSOperation

  • pthread:是一套基于 C 语言的通用多线程 API,线程的生命周期需要我们手动管理,使用难度较大,所以我们几乎不会使用。

  • NSThread:是一套面向对象的 API,线程的生命周期需要我们手动管理,简单易用,可直接操作线程对象。

  • GCD:是一套底层基于 C 语言的多线程 API,自动管理生命周期,充分利用了多核处理器的优点。

  • NSOperation:是一套底层基于 GCD 面向对象的多线程 API,自动管理生命周期,并且比 GCD 多了一些更简单实用的功能。

在这里我们暂且不讨论 pthread 的使用,主要看看后面三个方案都是怎么应用的。

NSThread

一个 NSThread 对象就代表一个线程,NSThread 有多种创建线程的方式:

1. 先创建再启动

// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"Axe"];
// 启动
[thread start];

2. 直接创建并启动线程

[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"Axe"];

3. 创建并启动

[self performSelectorInBackground:@selector(run:) withObject:@"Axe"];
[NSThread sleepForTimeInterval:2.0];

从三种创建线程的方法可以看到:方法 2和 3 都可以创建一个线程并且自启动,而方法一可以很方便的拿到线程对象。

4.应用示例

以下载一张图片为例:

NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
[thread start];
    
- (void)downloadImageWithURL:(NSURL *)url {
    NSError *error;
    NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
    if (error) {
        NSLog(@"error = %@",error);
    } else {
        UIImage *image = [UIImage imageWithData:data];
        [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    }
}

GCD

GCD 全称是 Grand Central Dispatch,可译为“中枢调度器”。GCD 是 Apple 公司为多核的并行运算提出的解决方案,它会自动利用更多的 CPU 内核来开启线程执行任务。

在了解 GCD 之前先明白同步/异步并行/串行这几个名词的概念。

  • 同步线程:在当前线程可立即执行任务,不具备开启线程的能力,会阻塞当前的线程,必须等待同步线程中的任务执行完并返回后,才会执行下一个任务。
  • 异步线程:在当前线程结束后执行任务,具备开启新的线程的能力。
  • 并行队列:允许多个任务同时执行。
  • 串行队列:只有等上一个任务执行完毕后,下一个任务才会被执行。

创建串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t mainQueue = dispatch_get_main_queue();

创建并行队列

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

创建同步+串行队列

// 在当前线程,立即执行任务
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"%@",[NSThread currentThread]);
    }
});

创建同步+并行队列

// 在当前线程,立即执行任务
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(concurrentQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"%d --- %@",i, [NSThread currentThread]);
    }
});

创建异步+串行队列

// 另开启线程,多个任务顺序执行
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d --- %@",i, [NSThread currentThread]);
        }
    });

    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d --- %@",i, [NSThread currentThread]);
        }
    });
});

创建异步+并行队列

// 另开启线程,多个任务一起执行,不分先后
dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{

    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d --- %@",i, [NSThread currentThread]);
        }
    });

    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d --- %@",i, [NSThread currentThread]);
        }
    });
});

应用示例

还是以下载一张图片为例

NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.download.image", DISPATCH_QUEUE_CONCURRENT);

__weak typeof(self) weakSelf = self;

dispatch_async(concurrentQueue, ^{

    NSError *error;

    NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];

    if (error) {
        NSLog(@"error = %@",error);
    } else {
        UIImage *image = [UIImage imageWithData:data];
        dispatch_async(dispatch_get_main_queue(), ^{
            weakSelf.imageView.image = image;
        });
    }
});

GCD其他函数的应用

dispatch_barrier 栅栏

功能描述:先执行栅栏前面的队列,再执行栅栏中的队列,等待栅栏中的队列执行完毕后,才会执行栅栏后面的队列。

注意事项:栅栏在并行队列中使用才有它的意义,强行在无序执行的队列中创造出顺序执行的队列任务,当不能使全局的并行队列,一般会自己创建我们的并行队列

代码示例:

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"1-->%d --- %@",i, [NSThread currentThread]);
    }
});

dispatch_async(concurrentQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"2-->%d --- %@",i, [NSThread currentThread]);
    }
});

dispatch_barrier_async(concurrentQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"3-->%d --- %@",i, [NSThread currentThread]);
    }
});

dispatch_async(concurrentQueue, ^{
    for (int i = 0; i < 10; i++) {
        NSLog(@"4-->%d --- %@",i, [NSThread currentThread]);
    }
});

通过 log 输出可以看到,1 和 2 队列的任务会先无序执行,在其两个队列中的任务执行完毕后,才会执行栅栏队列中的任务,最后才执行队列 4。

dispatch_after 延迟

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

});

dispatch_once 执行一次

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    //  仅仅执行一次
});

应用:常用用于单例中。

dispatch_apply 快速迭代

NSArray *array = @[@"a",@"b",@"c",@"d",@"e"];
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(array.count, globalQueue, ^(size_t idx) {
    NSLog(@"array[%zu] = %@",idx, array[idx]);
});

dispatch_group 队列组

功能描述:将多个队列添加到一个队列组中,当队列组中的任务都执行完毕后,会通知我们执行结果。

代码示例:

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, globalQueue, ^{
    NSLog(@"任务1");
});

dispatch_group_async(group, globalQueue, ^{
    NSLog(@"任务3");
});

dispatch_group_async(group, globalQueue, ^{
    NSLog(@"任务2");
});

dispatch_group_notify(group, globalQueue, ^{
    NSLog(@"所有任务都已完成");
});

dispatch_group_async(group, globalQueue, ^{
    NSLog(@"任务4");
});

GCD定时器

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);

// 3s 后定时器启动
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));

// 每1秒执行一次回调
dispatch_source_set_timer(timer, start, 2.0 * NSEC_PER_SEC, 0);

// 计时
__block int count = 0;

dispatch_source_set_event_handler(timer, ^{
    NSLog(@"%d", count);
    if (count > 10) {
        dispatch_cancel(timer);
    }
    count++;
});
dispatch_resume(timer);

NSOperation

NSOperation 是一个抽象类,并不具备封装操作的能力,我们必须使用它的子类。为此系统也提供了NSInvocationOperationNSBlockOperation 两个子类供我们使用。当然我们也可以继承 NSOperation,创建我们的子类,实现内部相应的方法。

NSInvocationOperation

NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
[invocationOperation start];

默认情况下,调用 start 方法后,并不会开启一个新的线程去执行 selector,而是在当前线程同步的执行操作,只有将 NSOperation 添加到 NSOperationQueue 中,才会执行异步操作。

NSBlockOperation

NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    // 在主线程中执行
    NSLog(@"操作1 --- %@", [NSThread currentThread]);
}];

[blockOperation addExecutionBlock:^{
    // 在子线程中执行
    NSLog(@"操作2 --- %@", [NSThread currentThread]);
}];

// 添加额外的任务数大于1才会异步执行

[blockOperation addExecutionBlock:^{
    // 在子线程中执行
    NSLog(@"操作3 --- %@", [NSThread currentThread]);
}];

[blockOperation addExecutionBlock:^{
    // 在子线程中执行
    NSLog(@"操作4 --- %@", [NSThread currentThread]);
}];

[blockOperation start];

Operation 的其它用法

执行操作

执行一个 Operation 有两种方式,一种是手动调用 start,这种方法调用会在当前线程进行同步执行,因此在主线程里面一定要小心调用,不然就会堵塞主线程。另一种是自动执行,只要将 operation 添加到 operationQueue 中就会尽快执行操作。

取消操作

NSOperation 开始执行操作后,会默认一直到操作完成,当然中途我们也可以调用 cancel 方法取消操作。

在调用 cancel 方法的时候,只是将 cancelled 设置为了 YES,因此在每个操作开始前,或者在每个有意义的实际操作完成后,都要先检测 isCancelled 是否被设置成了 YES, 如果已经取消了,那么后面的操作就不用在执行了。

操作完成后的操作

如果想在 NSOperation 执行完操作后做一些事情,可以调用 completionBlock 设置,在操作完成后就会回调block里面的内容。

自定义 Operation

如果系统提供的 NSInvocationOperationNSBlockOperation 两个子类都不能满足自己的需求时,我们通过继承自定义子类,并添加需要执行的操作。

这里我们仍然以下载图片为例:

//
//  MyOperation.h
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef void(^MyDownloadFinishedBlock)(UIImage *image);

@interface MyOperation : NSOperation
@property (copy, nonatomic, readonly) NSString *imageURL;

- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock;
@end

//
//  MyOperation.m
//

#import "MyOperation.h"

@interface MyOperation ()
@property (copy, nonatomic) MyDownloadFinishedBlock downloadFinishedBlock;
@end

@implementation MyOperation

- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock {
    self = [super init];
    if (self) {
        _imageURL = urlString;
        _downloadFinishedBlock = downloadFinishedBlock;
    }
    return self;
}

- (void)main {
    
    // 如果时在异步线程中执行操作,即 main 方法在异步线程中调用,那么将无法访问主线程的自动释放池,因此创建一个属于当前线程的自动释放池
    
    @autoreleasepool {
        
        // 在 main 方法中定期的调用 isCancelled 方法检测操作是否已经被取消
        // 在执行任何实际的工作之前,也就是 main 方法的开头,就要检测操作是否已经被取消
        // 在执行一段耗时的操作后也需要检测操作是否已经被取消
        
        if (self.isCancelled) {
            return;
        }
        
        NSURL *url = [NSURL URLWithString:_imageURL];
        
        // 此处就是通过网络获取图片 data,是比较耗时的操作,所以在后面就需要检测操作是否已经被取消
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        
        if (self.isCancelled) {
            url = nil;
            _imageURL = nil;
            
            return;
        }
        
        UIImage *image = [UIImage imageWithData:imageData];
        
        if (self.isCancelled) {
            image = nil;
            
            return;
        }
        
        if (_downloadFinishedBlock) {
            _downloadFinishedBlock(image);
        }
        
    }
    
}
@end

// 使用自定义的 MyOperation 下载一张图片
MyOperation *operation = [[MyOperation alloc] initWithURLString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg" downloadFinishedBlock:^(UIImage *image) {
    NSLog(@"%@",[NSThread currentThread]);

    [_imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
}];

[operation start];

说明:如果你要执行一个同步操作,那只需要重写 main 方法,在里面添加必要的操作。如果要执行一个异步的操作,那就需要重写 start 方法,因为在你把操作添加进去后系统会自动调用 start 方法,这时将不在调用mian里面的操作。

NSOperationQueue

一个 NSOperation 对象可以通过 start 方法来执行任务,默认是同步执行的。也可以将其添加到NSOperationQueue 操作队列中去执行,它是异步执行的。

添加 NSOperation 到 NSOperationQueue 中

不管通过哪种方式添加,只要将 operation 添加到 operationQueue 中,通常短时间内就会得到执行(异步)。但是如果存在依赖,或者整个 operationQueue 被暂停等原因,也可能需要等待。

添加一个operation

[operationQueue addOperation:invocationOperation];

添加一组operation

[operationQueue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:YES];

添加一个block形式的operation

[operationQueue addOperationWithBlock:^{
    NSLog(@"任务");
}];

添加依赖

概述

所谓依赖就是说,当某个 operation 对象需要依赖于其它 operation 对象才能完成时,就可通过 addDependency 方法添加一个或者多个依赖对象,只有所有依赖的对象都已经完成操作后,当前的 operation 对象才开始执行操作,当然也可以通过 removeDependency 方法来移除这种依赖关系。

依赖方式

既可以在同一个 operationQueue 中不同 operation 对象添加依赖,也可以在不同的 operationQueue 之间不同operation 对象之间添加依赖,operation 对象会管理自己的依赖关系。

限制依赖

虽然可以在一个 queue 中添加依赖,也可以在不同的 queue 中添加依赖,但是要特别注意的是,不能添加环形依赖,即 a 依赖 b,b 也依赖 a。

应用示例:

这里以修改用户头像为例,模拟从用户上传头像到显示头像的过程。

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
        
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1.上传头像");
}];

NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"2.从服务器请求上传头像的url");
}];

NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3.通过url下载头像");
}];

[operationQueue addOperation:blockOperation1];
[operationQueue addOperation:blockOperation2];
[operationQueue addOperation:blockOperation3];

打印顺序如下:2 - 1 - 3 或者 3 - 2 -1 

这样的顺序显然不符合正常的逻辑顺序,这时候,我们就可以通过添加依赖,达到我们说期望的顺序逻辑。

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
        
NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1.上传头像");
}];

NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"2.从服务器请求上传头像的url");
}];

NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3.通过url下载头像");
}];


// 3 -> 2  2 -> 1 (3 - 2 - 1)
[blockOperation3 addDependency:blockOperation2];
[blockOperation2 addDependency:blockOperation1];


[operationQueue addOperation:blockOperation1];
[operationQueue addOperation:blockOperation2];
[operationQueue addOperation:blockOperation3];

打印顺序如下:1 - 2 - 3

这样就符合我们期望的逻辑,即用户先上传图片并完成后,再从服务器获取该用户头像的 url 地址,最后通过 url 地址下载相应的头像并显示。