iOS多线程详解

前面已经有一篇文章(学习GCD看我就够了)专门介绍了GCD,下面来介绍一下另外三个与多线程相关的方法

一、pthreads(现在几乎不用了)

pthread是POSIX thread的简写,一套通用的多线程API,适用于Unix、Linux、Windows等系统,跨平台、可移植,使用难度大,C语言框架,线程生命周期由程序员管理,由于iOS开发几乎用不到,以下就简单运用pthread开启一个子线程,用来处理耗时操作

// 创建线程,并且在线程中执行 demo 函数
- (void)pthreadDemo {
     返回值:
     - 若线程创建成功,则返回0
     - 若线程创建失败,则返回出错编号
     */
    pthread_t threadId = NULL;
    NSString *str = @"Hello Pthread";
    // 这边的demo函数名作为第三个参数写在这里可以在其前面加一个&,也可以不加,因为函数名就代表了函数的地址。
    int result = pthread_create(&threadId, NULL, demo, (__bridge void *)(str));

    if (result == 0) {
        NSLog(@"创建线程 OK");
    } else {
        NSLog(@"创建线程失败 %d", result);
    }
    // pthread_detach:设置子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
    pthread_detach(threadId);
}

// 后台线程调用函数
void *demo(void *params) {
    NSString *str = (__bridge NSString *)(params);

    NSLog(@"%@ - %@", [NSThread currentThread], str);
    return NULL;
}
二、NSThread

NSThread是基于线程使用,轻量级的多线程编程方法(相对GCD和NSOperation),一个NSThread对象代表一个线程,需要手动管理线程的生命周期,处理线程同步等问题。

创建线程
  • 方法一
// 1. 创建线程
 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:nil];
    thread.name = @"thread1"; //设置线程名
    [thread start];   // 2. 启动线程,此方法需要我们手动开启线程
- (void)test:(NSString *)string {
  NSLog(@"test - %@ - %@", [NSThread currentThread], string);
}

打印如下:

2017-09-29 10:28:44.203914+0800 aegewgr[9577:3142906] test - <NSThread: 0x600000461a40>{number = 3, name = thread1} - (null)

这里我们最好设置一下线程名,便于我们的调试

  • 方法二
[NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"分离子线程"];

该方法会自动创建一个子线程,并在子线程中执行

2017-09-29 10:33:21.702512+0800 aegewgr[9617:3159015] test - <NSThread: 0x6000004621c0>{number = 4, name = (null)} - 分离子线程
  • 方法三
[self performSelectorInBackground:@selector(test:) withObject:@"后台线程"];

该方法会开启一条后台线程,并在后台线程中执行。
上面所有的方法都还有与之对应的通过block创建的方法。

另外通过线程间通信的几个方法

[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]

[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]

关于进程间通信,可以看我另一篇文章Runloop的应用与深入理解

还有一下常用的属性和方法直接去看文档就可以了。

三、NSOperation和NSOperationQueue

NSOperation是基于GCD开发的,但是比GCD拥有更强的可控性和代码可读性。NSOperation是一个抽象基类,表示一个独立的计算单元,可以为子类提供有用且线程安全的建立状态,优先级,依赖和取消等操作。我们使用比较多的就是它的子类NSInvocationOperation和NSBlockOperation。不过我们更多的使用是自己继承并定制自己的操作。

使用NSInvocationOperation

    NSInvocationOperation *invo = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(test:) object:nil];
    [invo start];
    NSLog(@"111");
- (void)test:(NSString *)string {
    sleep(1);
    NSLog(@"test - %@ - %@", [NSThread currentThread], string);
}
2017-09-29 14:19:13.242517+0800 aegewgr[10143:3388734] test - <NSThread: 0x600000078180>{number = 1, name = main} - (null)
2017-09-29 14:19:13.242967+0800 aegewgr[10143:3388734] 111

可以看到NSInvocationOperation是同步并且串行的,所以只是用NSInvocationOperation并没有什么卵用,主要还是要和NSOperationQueue结合使用。这个放到后面再讲。

使用NSBlockOperation
NSBlockOperation支持并发的实行一个或多个block,使用起来非常方便

NSBlockOperation *blockOperation = [[NSBlockOperation alloc]init];
    [blockOperation addExecutionBlock:^{
        NSLog(@"block 1 in thread:%@",[NSThread currentThread]);
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"block 2 in thread:%@",[NSThread currentThread]);
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"block 3 in thread:%@",[NSThread currentThread]);
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"block 4 in thread:%@",[NSThread currentThread]);
    }];
    [blockOperation addExecutionBlock:^{
        sleep(1);
        NSLog(@"block 5 in thread:%@",[NSThread currentThread]);
    }];
    [blockOperation start];
    NSLog(@"123");
2017-09-29 14:32:03.710936+0800 aegewgr[10335:3439694] block 1 in thread:<NSThread: 0x60400006d740>{number = 1, name = main}
2017-09-29 14:32:03.710939+0800 aegewgr[10335:3439916] block 3 in thread:<NSThread: 0x60400027a780>{number = 4, name = (null)}
2017-09-29 14:32:03.710943+0800 aegewgr[10335:3439920] block 4 in thread:<NSThread: 0x60400027a840>{number = 5, name = (null)}
2017-09-29 14:32:03.710961+0800 aegewgr[10335:3439919] block 2 in thread:<NSThread: 0x600000270c00>{number = 3, name = (null)}
2017-09-29 14:32:04.712532+0800 aegewgr[10335:3439920] block 5 in thread:<NSThread: 0x60400027a840>{number = 5, name = (null)}
2017-09-29 14:32:04.712932+0800 aegewgr[10335:3439694] 123

注意,这里我故意让block5 sleep了1秒才执行。而123这个输出也是直到block5执行完了才执行,所以,NSBlockOperation也是同步的,而block的执行是并发的。至于串行队列并发队列与同步异步的概念可以参考我前面提到的那篇文章

自定义NSOperation

自定义NSOperation分两种,一种是自定义非并发的NSOperation,一种是定义并发的NSOperation的。下面分别介绍。

  • 定义非并发的NSOperation

如果是自定义非并发的NSOperation,只需要重写main方法就够了。

#import "SerialNSOperation.h"

@implementation SerialNSOperation

- (void)main
{
    NSLog(@"main begin");
    @try {
        //在这里我们要创建自己的释放池,因为这里我们拿不到主线程的释放池
        @autoreleasepool {
            // 提供一个变量标识,来表示需要执行的操作是否完成了,当然,没开始执行之前,为NO
            BOOL taskIsFinished = NO;
            // while 保证:只有当没有执行完成和没有被取消,才执行自定义的相应操作
            while (taskIsFinished == NO && [self isCancelled] == NO){
                // 自定义的操作
                NSLog(@"currentThread = %@", [NSThread currentThread]);
                sleep(10);  // 模拟耗时操作
                // 这里相应的操作都已经完成,后面就是要通知KVO我们的操作完成了。
                taskIsFinished = YES;
            }
        }
    }
    @catch (NSException * e) {
        NSLog(@"Exception %@", e);
    }
    NSLog(@"main end");
}

然后直接使用

SerialNSOperation *op = [[SerialNSOperation alloc]init];
[op start];
2017-09-29 15:12:59.151481+0800 aegewgr[10524:3564080] main begin
2017-09-29 15:13:09.152082+0800 aegewgr[10524:3564080] currentThread = <NSThread: 0x60400006e1c0>{number = 1, name = main}
2017-09-29 15:13:09.152299+0800 aegewgr[10524:3564080] main end

其实我感觉这个实用性不大。

  • 定义并发的NSOperation

自定义并发的NSOperation需要以下步骤:
1.start方法:该方法必须实现,
2.main:该方法可选,如果你在start方法中定义了你的任务,则这个方法就可以不实现,但通常为了代码逻辑清晰,通常会在该方法中定义自己的任务
3.isExecuting isFinished 主要作用是在线程状态改变时,产生适当的KVO通知
4.isAsynchronous :必须覆盖并返回YES;

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

@interface ConcurrentOperation : NSOperation{
    BOOL executing;
    BOOL finished;
}
//.m
#import "ConcurrentOperation.h"

@implementation ConcurrentOperation
- (id)init {
    if(self = [super init])
    {
        executing = NO;
        finished = NO;
    }
    return self;
}
- (BOOL)isAsynchronous {
    return YES;
}
- (BOOL)isExecuting {
    return executing;
}
- (BOOL)isFinished {
    return finished;
}
- (void)start {
    //第一步就要检测是否被取消了,如果取消了,要实现相应的KVO
    if ([self isCancelled]) {
        [self willChangeValueForKey:@"isFinished"];
        finished = YES;
        [self didChangeValueForKey:@"isFinished"];
        return;
    }
    //如果没被取消,开始执行任务
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    executing = YES;
    [self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
    NSLog(@"main begin");
    @try {
        @autoreleasepool {
            //在这里定义自己的并发任务
            NSLog(@"自定义并发操作NSOperation");
            NSThread *thread = [NSThread currentThread];
            NSLog(@"current Thread:%@",thread);
            //任务执行完成后要实现相应的KVO
            [self willChangeValueForKey:@"isFinished"];
            [self willChangeValueForKey:@"isExecuting"];
            executing = NO;
            finished = YES;
            [self didChangeValueForKey:@"isExecuting"];
            [self didChangeValueForKey:@"isFinished"];
        }
    }
    @catch (NSException * e) {
        NSLog(@"Exception %@", e);
    }
    NSLog(@"main end");
}
//调用
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    ConcurrentOperation *op1 = [[ConcurrentOperation alloc]init];
    ConcurrentOperation *op2 = [[ConcurrentOperation alloc]init];
    ConcurrentOperation *op3 = [[ConcurrentOperation alloc]init];
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];    

打印如下:

2017-09-29 15:34:15.158649+0800 aegewgr[10664:3638228] main begin
2017-09-29 15:34:15.158653+0800 aegewgr[10664:3638226] main begin
2017-09-29 15:34:15.158675+0800 aegewgr[10664:3638227] main begin
2017-09-29 15:34:15.158912+0800 aegewgr[10664:3638228] 自定义并发操作NSOperation
2017-09-29 15:34:15.159321+0800 aegewgr[10664:3638226] 自定义并发操作NSOperation
2017-09-29 15:34:15.159372+0800 aegewgr[10664:3638227] 自定义并发操作NSOperation
2017-09-29 15:34:15.159965+0800 aegewgr[10664:3638226] current Thread:<NSThread: 0x60400046b640>{number = 4, name = (null)}
2017-09-29 15:34:15.160014+0800 aegewgr[10664:3638228] current Thread:<NSThread: 0x60000026d140>{number = 5, name = (null)}
2017-09-29 15:34:15.160103+0800 aegewgr[10664:3638227] current Thread:<NSThread: 0x60400046b5c0>{number = 3, name = (null)}
2017-09-29 15:34:15.160799+0800 aegewgr[10664:3638226] main end
2017-09-29 15:34:15.160973+0800 aegewgr[10664:3638227] main end
2017-09-29 15:34:15.161154+0800 aegewgr[10664:3638228] main end

为了展示并发执行,所以我这里使用了NSOperationQueue,后面我在继续讲这个。

使用NSOperationQueue

NSOperationQueue就是执行NSOperation的队列,我们可以将一个或多个NSOperation对象放到队列中去执行。NSOperationQueue有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

使用起来很简单:

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  //主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        //任务执行
}];
[queue addOperation:operation];

我们可以通过设置 maxConcurrentOperationCount 属性来控制并发任务的数量,当设置为 1时, 那么它就是一个串行队列。主对列默认是串行队列,这一点和 dispatch_queue_t是相似的。前面也说过,NSOperation就是基于GCD开发的。
NSOperationQueue相对于GCD来说有以下优点:

  • 提供了在 GCD 中不那么容易复制的有用特性。
  • 可以很方便的取消一个NSOperation的执行
  • 可以更容易的添加任务的依赖关系
  • 提供了任务的状态:isExecuteing, isFinished.

以上就是多线程相关的所有方法了,具体使用什么方法还是看你的需求。如果我讲的有什么错误的地方希望大家指正。

推荐阅读更多精彩内容

  • 欢迎大家指出文章中需要改正或者需要补充的地方,我会及时更新,非常感谢。 一. 多线程基础 1. 进程 进程是指在系...
    xx_cc阅读 5,281评论 15 61
  • 1、简介 NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高...
    WQ_UESTC阅读 232评论 0 5
  • Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么? 1...
    AlanGe阅读 763评论 0 14
  • 在了解GCD之前,我们首先要知道几个概念。关于队列和同/异步函数。为了让读者更简单直观的理解这些概念,我尽可能用最...
    fou7阅读 432评论 1 2
  • 什么是进程? 进程是指在系统中正在运行的一个应用程序。 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存...
    珍此良辰阅读 421评论 1 5