Objective-C-(四)-多线程

介绍多线程前先来理解下进程和线程的概念:

进程:一个在前台正在运行的应用程序就是一个进程。比如打开的微信APP就是一个进程。

线程:微信APP可以聊天,发图片,而做这些事情都是要通过线程来做的。线程就是执行任务的基本单元,是CPU调度和分派的基本单位。一个进程可以有多个线程,线程是进程的一部分。

多线程就是多个线程并发处理任务的技术,可以充分利用多核CPU的资源,提升执行性能。

iOS中处理和操作线程的方案有PthreadsNSThreadGCDNSOperation && NSOperationQueuePthreads 比较底层,没有用过,就不说了,主要来说下NSThreadGCDNSOperation && NSOperationQueue

介绍之前先说下两个概念:同步/异步,串行/并发

  • 同步是指在执行任务的时候,会等待当前这个任务执行完毕后再继续向下执行。如果当前这个任务没有执行完毕,那么就会阻塞当前的线程直到这个任务执行完成。

  • 异步是指在执行任务的时候,不会等待当前这个任务执行完毕就会继续向下执行。即使当前这个任务没有执行完,也会立刻执行下面的任务。

同步异步的区别可以理解为:例如方法A内部中有个方法B,当前的方法A内部执行到了方法B,如果是同步,会等这个方法B执行完返回后才会向下执行,如果方法B没有返回,当前的线程就会一直卡住不向下执行;如果是异步的话这个方法B会立刻返回,然后继续向下执行。

//方法A
- (void)methodA {
 ....其他任务
 [self methodB]
 ....其他任务
}
//方法B
- (void)methodB {
 ...
}
  • 串行:执行任务的时候按顺序执行,一次只能执行一个任务。当前的任务没有执行完,不会执行下一个。

  • 并发:执行任务的时候可以多个任务同时执行。

NSThread

NSThread是一个直接面向线程的类,提供了很多可以操作线程的方法。最简单的创建一个线程:

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doThead:) object:@"zzy"];

这样就创建了一个线程实例threadthread通过doThead:这个方法执行任务。但是这个时候创建的线程并没有启动执行任务,需要调用下start方法:[thread start];这样线程就开始执行任务了。object:@"zzy"参数会在线程执行doThead:方法的时候被作为参数传入。

我们也可以给一个线程设置一个名字作为标记:[thread setName:@"zzy"]; 通过[NSThread currentThread]获取当前线程的信息,打印出当前这个线程可以看到:

XXX[67222:788960] thread = {number = 3, name = zzy}

通过name我们可以找到我们设置的是哪个thread

NSThread 也提供了一些关于线程的状态信息,例如:executingfinishedcancelled,分别表示当前的线程正在执行,完成,取消的状态。但是cancelled只是一个标记状态,并不会取消线程,如果想强制退出当前的线程,可以通过[NSThread exit];

NSThread的使用比较简单,主要是注意通过NSThread创建的子线程中指定NSTimer计时器的情况。由于子线程中的runloop默认不启动,所以当添加一个定时器重复执行任务的时候要指定启动runloop。

- (void)doThead:(NSString *)object {    
   NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRepeatAction:) userInfo:@{@"id":@"zzy"} repeats:YES];
   [timer fire];
   [[NSRunLoop currentRunLoop] run];
}

GCD

GCD是平时最常用的多线程处理的方案了,我们将任务放进block中追加到队列里,系统就会自动为我们创建相应的线程去处理任务,并且任务完成后在合适的时机销毁线程,整个过程中不需要我们去直接操作线程,所以使用起来比较方便。

GCD有三种队列:串行队列,并发队列,主队列(特殊的串行队列,队列中的任务一定会在主线程中执行)

GCD获取队列的方式有两种:

  • 第一种通过dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);函数直接创建队列,第一个参数的队列的名称,方便调试使用,第二个是队列的类型。传NULLDISPATCH_QUEUE_SERIAL表示串行队列,传DISPATCH_QUEUE_CONCURRENT表示并发队列。
    dispatch_queue_t queue = dispatch_queue_create("zzy", NULL);
  • 第二种是直接获取系统提供好的两个队列:

    • dispatch_get_main_queue:主队列,运行在主线程中的串行队列

    • dispatch_get_global_queue:全局队列,也就是并发队列,通过这个函数的第一个参数还可以指定队列的优先级。

GCD中平时常用的有以下几个函数:

  • dispathc_once:确保函数中的block只执行一次,而且是线程安全的。常用来实现单利对象。

  • dispatch_after:延迟指定的时间之后提交某个任务(这里是延迟某个时间提交任务,而不是延迟某个时间执行任务)。常用做一个定时操作。

  • dispatch_suspend&&dispatch_resume:暂停和恢复某个队列

  • dispatch_apply:循环将某些任务加入到某个队列当中

  • dispatch_set_target_queue:可以设置目标队列的优先级,让目标队列的优先级和指定的队列优先级一样。(由于dispatch_queue_create函数创建的队列没有提供设置优先级的参数,默认是默认的优先级,所以可以用这个函数来设置其他优先级,例如dispatch_set_target_queue(targetQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));

  • dispatch_barrier_async:在一个并发队列中,先并行处理一部分任务,然后同步执行dispatch_barrier_async中的任务(只执行当前block中的一个任务,其他任务不执行),dispatch_barrier_async任务执行完成后,然后再恢复当前队列并行执行的任务。常用来处理在多线程数据读取的时候插入写入操作(写入操作必须是线程安全的)。

这里主要介绍下平时处理异步任务的时候比较有用的任务组dispatch_group_t和信号量dispatch_semaphore_t

dispatch_group_t

dispatch_group_t是一个任务组,可以将几个并发任务一起放到任务组里面,当这几个并发任务都执行完成后,同步得到通知回调。涉及到两个常用的函数:dispatch_waitdispatch_notify

dispatch_wait是一个同步的函数,一旦被调用该函数就会一直处于调用的状态而不返回,直到dispatch group内的任务都执行完成或者经过dispatch_wait中第二参数指定的时间后它才会返回,否则它会一直阻塞当前的线程,无法继续执行。如果dispatch_wait函数返回值为0,说明group内的任务已经执行完毕;如果返回值不为0,说明经过了指定的时间group内的任务依然没有执行完。

- (void)dispathGroup {
/**
 使用dispatch_group_t也可以向group中指定不同优先级的队列,不一定非要是同一个队列,他们都归属同一个group
*/
 dispatch_group_t group = dispatch_group_create();
 dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
 dispatch_group_async(group, defaultQueue, ^{
 sleep(2);
 NSLog(@"任务一");
 });
 dispatch_group_async(group, defaultQueue, ^{
 sleep(2);
 NSLog(@"任务二");
 });
 dispatch_group_async(group, highQueue, ^{
 NSLog(@"高优先级任务");
 });
 dispatch_wait(group, DISPATCH_TIME_FOREVER);
 NSLog(@"任务执行完成");
}

执行后:

2019-01-01 10:09:53.092345+0800 XXX[12671:1433314] 高优先级任务
2019-01-01 10:09:55.094704+0800 XXX[12671:1433327] 任务二
2019-01-01 10:09:55.094704+0800 XXX[12671:1433313] 任务一
2019-01-01 10:09:55.095105+0800 XXX[12671:1433235] 任务执行完成

可以看到dispatch_wait 会一直阻塞当前的线程,直到任务执行完成(这里指定的时间是永久等待,也可以自己定义临界时间)。

dispatch_notify提供了一个异步执行的函数,它不会阻塞当前的线程,但是会监听dispatch group的任务执行情况,一旦group内的都执行完成后就会调用dispatch_notify函数。dispatch_notify(object, queue, notification_block)函数提供了三个参数,第一个参数就是监听的dispatch group,另外两个参数提供了可以让某个任务在指定队列中执行的功能。也就说当dispatch group内的任务都完成后会通知dispatch_notify然后可以进行一些完成后的处理操作。

- (void)dispathGroup {
 dispatch_group_t group = dispatch_group_create();
 dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
 dispatch_group_async(group, defaultQueue, ^{
 sleep(2);
 NSLog(@"任务一");
 });
 dispatch_group_async(group, defaultQueue, ^{
 sleep(2);
 NSLog(@"任务二");
 });
 dispatch_group_async(group, highQueue, ^{
 NSLog(@"高优先级任务");
 });

 dispatch_notify(group, dispatch_get_main_queue(), ^{
 NSLog(@"dispatch group执行完成");
 });
 NSLog(@"任务执行完成");
}

执行后:

2019-01-01 10:28:40.864817+0800 XXX[12801:1454449] 任务执行完成
2019-01-01 10:28:40.864821+0800 XXX[12801:1454557] 高优先级任务
2019-01-01 10:28:42.865759+0800 XXX[12801:1454555] 任务二
2019-01-01 10:28:42.865760+0800 XXX[12801:1454558] 任务一
2019-01-01 10:28:42.866070+0800 XXX[12801:1454449] dispatch group执行完成

dispatch_notify函数提供的这种获取dispatch group内任务完成后的通知,然后再异步执行处理很常用。

如果对同步、异步理解的不够深刻的话,使用dispatch_group_async函数的时候可能会导致一个错误的认识。比如在dispatch_group_async中添加几个网络请求的任务,会发现请求还没完成就执行了dispatch_notify回调。这是因为发起网络请求本身就是一个任务,在把这个任务通过block追加到dispatch_group_async中的队列后就算完成了,并不会等待网络请求的完成,因为网络请求本身也是异步的。所以在遇到这种异步操作情况的时候,可以用另外一对函数将来判定任务完成情况:dispatch_group_enter(添加到group中)、dispatch_group_leave(执行完成退出group)。

这两个函数必须同时存在,类似于引用计数,这是对group内的任务的递增和递减,如果只有递增没有递减,那么group的任务就会永远执行不完,也就一直不会回调dispatch_notify函数,或者dispatch_wait函数一直不会返回。

- (void)dispathGroup {
 dispatch_group_t group = dispatch_group_create();
 dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

 dispatch_group_enter(group);
 dispatch_async(defaultQueue, ^{
 sleep(2);
 NSLog(@"任务一");
 dispatch_group_leave(group);
 });

 dispatch_group_enter(group);
 dispatch_async(defaultQueue, ^{
 sleep(2);
 NSLog(@"任务二");
 dispatch_group_leave(group);
 });

 dispatch_group_enter(group);
 dispatch_async(highQueue, ^{
 NSLog(@"高优先级任务");
 dispatch_group_leave(group);
 });

 dispatch_notify(group, dispatch_get_main_queue(), ^{
 NSLog(@"dispatch group执行完成");
 });
 NSLog(@"任务执行完成");
}

dispatch_semaphore_t

dispatch_semaphore_t 信号量一共就三个函数,用起来比较简单,只是需要结合不同的场景去理解才能发挥很大的用处:

  • dispatch_semaphore_create : 创建一个信号量,并指定信号的初始化个数

  • dispatch_semaphore_wait:锁住当前的线程,等待信号的计数大于等于1,然后将计数减去1,并且该函数返回。如果信号是0,则该函数一直不返回,阻塞当前的线程。

  • dispatch_semaphore_signal:释放信号,让信号计数加1

在很多开源库中都使用dispatch_semaphore_t作为锁来处理多线程时对数据写入的保护。例如下面示例代码

- (void)dispatchSemaphore {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            [array addObject:[NSNumber numberWithInt:i]];
            dispatch_semaphore_signal(semaphore);
        });
    }
}

在一个异步并发线程中,对数组进行添加元素的操作。由于会有多个线程同时对array进行写入操作,如果不加dispatch_semaphore_t则很可能会导致内存访问错误导致程序终止。

加了dispatch_semaphore_t后,将信号的计数指定为1,每当执行一次dispatch_semaphore_wait函数后,信号就会减1,此时信号为0,则阻塞线程无法执行下面的[array addObject:[NSNumber numberWithInt:i]];方法,直到前面的线程执行完对数组的操作然后执行dispatch_semaphore_signal函数使信号加1,当前的线程才能访问数组,这样一来,每次对数组的访问操作都只能有一个线程,就保护了数据访问的安全。当然对本例也可以用其他方法来处理,例如指定一个串行队列。

通过dispatch_semaphore_t信号量和dispatch_group_t的组合,也能实现代替上面例子中使用dispatch_group_enterdispatch_group_leave的效果。

- (void)dispatchSemaphoreGroup {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_async(group, defaultQueue, ^{
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        dispatch_async(defaultQueue, ^{
            sleep(2);
            NSLog(@"任务一");
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    
    dispatch_group_async(group, defaultQueue, ^{
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        dispatch_async(defaultQueue, ^{
            sleep(2);
            NSLog(@"任务二");
            dispatch_semaphore_signal(semaphore);
        });
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    });
    
    dispatch_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dispatch group执行完成");
    });
    NSLog(@"任务执行完成");
}

在上面的代码中,每次追加block中的任务到group的时候,都先创建一个信号计数为0的信号量,然后开启一个block内部的异步任务的执行,在block的最后使用dispatch_semaphore_wait锁住当前的线程,让当前的block无法返回,则group的任务就一直无法完成。直到block内部的这个异步任务执行完成后释放信号,通过dispatch_semaphore_signal让信号加1,则block返回,group内的任务完成,再回调dispatch_notify函数。

信号量在网络请求的同步处理,资源竞争等情况下可以发挥很大的用处,可以使用信号量来解决这些问题。

NSOperation && NSOperationQueue

相对于GCD都是纯C的函数,NSOperation提供了一个更高层面面向对象的多线程处理方案。NSOperation是用来封装任务的一个类。我们把需要执行的任务封装进NSOperation实例当中,通过调用start方法它会自动去执行这些任务。它也提供了很多种的状态便于我们观测了解当前任务的执行状态,例如:isCancelledisReadyisExecutingisFinished等。NSOperation是一个抽象类,我们不能直接使用NSOperation类的去处理任务,系统提供了两个子类NSInvocationOperationNSBlockOperation方便我们使用,也可以自己创建一个NSOperation的子类去实现。

NSInvocationOperation提供了一种target - action模式响应的处理任务类,通过指定target,去对应执行action操作。例如:

- (void)invocationOperationAction {
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationAction:) object:@"zzy"];
    [operation start];
}

- (void)invocationOperationAction:(id)userInfo {
    NSLog(@"userInfo = %@", userInfo);
}

NSBlockOperation则提供了block的方式去处理任务,通过把任务封装进block块中对应去执行。

NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"operation1");
}];
[operation start];

还可以给operation中添加多个任务:

[operaion addExecutionBlock:^{
      NSLog(@"operation2");
}];
[operaion addExecutionBlock:^{
      NSLog(@"operation3");
}];
[operation start];

只有当这些添加的Block中的任务都执行完,这个operation才算执行完成。

需要注意的是,如果通过手动执行NSOperation的start方法去启动任务的话,start方法是同步的。NSOperation提供了一个只读属性isAsynchronous(也可以通过concurrent)来标识当前NSOperation是否是异步执行,默认这个属性是NO。如同上面关于同步的解释,start方法会阻塞当前调用它的线程,直到operation的操作都完成。看下例子:

- (void)blockOperationThread {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(synchronousOperation) object:nil];
    [thread start];
}

- (void)synchronousOperation {
    NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
        sleep(2);
        NSLog(@"operationBlock1");
        NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
    }];
    [operaion addExecutionBlock:^{
        NSLog(@"operationBlock2");
        NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
    }];
    [operaion start];
    NSLog(@"synchronousOperation....");
}

打印如下:

2019-01-01 11:17:30.504408+0800 XXX[11341:1269966] operationBlock2
2019-01-01 11:17:30.504966+0800 XXX[11341:1269966] operation2 Thread = {number = 4, name = (null)}
2019-01-01 11:17:32.506224+0800 XXX[11341:1269987] operationBlock1
2019-01-01 11:17:32.506513+0800 XXX[11341:1269987] operation1 Thread = {number = 3, name = (null)}
2019-01-01 11:17:32.506980+0800 XXX[11341:1269987] synchronousOperation....

可以看到直到两秒之后才执行NSLog(@"synchronousOperation....");也就是说NSOperation的执行是同步的。同时我们通过打印执行NSOperation任务的线程发现,同一个operaion内添加的多个任务,执行这些任务的线程并不是同一个。也就是说虽然NSOperation是同步的,但是operaion内部添加的任务可能是在不同线程中并发执行的,这些任务执行完成后才会向下执行,有点类似GCD的dispatch_group

那么如何实现异步执行的NSOperation呢?可以通过两种方式:一种是通过创建NSOperation子类自己去实现异步操作,另外一种是通过NSOperationQueue。我们平时用的比较多的是通过使用NSOperationQueue来实现异步处理NSOperation。

NSOperationQueue

通过NSOperationQueue来实现NSOperation的执行比较简单,直接将NSOperation放进NSOperationQueue的队列中就可以了。NSOperationQueue会自动为NSOperation创建线程并且调用它的start方法启动任务。当然NSOperation添加到NSOperationQueue中后也不一定会立刻被执行,如果NSOperationQueue中有很多个operation,当前的operation会在队列排到它以后自动执行。

通过NSOperationQueue来执行operation:

//将上面的代码改为NSOperationQueue来执行
- (void)synchronousOperation {
    NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
        sleep(2);
        NSLog(@"operationBlock1");
        NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
    }];
    [operaion addExecutionBlock:^{
        NSLog(@"operationBlock2");
        NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
    }];

//    [operaion start];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operaion];
    NSLog(@"synchronousOperation");
}

打印如下:

2019-01-01 11:42:41.789280+0800 XXX[11463:1298262] synchronousOperation
2019-01-01 11:42:41.789611+0800 XXX[11463:1298233] operationBlock2
2019-01-01 11:42:41.791127+0800 XXX[11463:1298233] operation2 Thread = {number = 4, name = (null)}
2019-01-01 11:42:43.790340+0800 XXX[11463:1298241] operationBlock1
2019-01-01 11:42:43.790851+0800 XXX[11463:1298241] operation1 Thread = {number = 5, name = (null)}

可以看到operation不会阻塞当前的线程,会自动的异步去执行任务。

自定义NSOperation

通过自定义NSOperation可以实现更多的定制化任务处理,例如异步执行NSOperation。自定义NSOperation要实现以下几个方法:

  • start :在start方法中实现异步处理的操作,不论是调用异步处理函数还是说开启新的线程。而且要及时通过KVO更新operation当前的状态:isExecutingisFinished,以便于通知它的监测者。

  • main 官方文档中建议在这个方法中实现执行任务的操作。当然也可以不实现这个方法,直接在start中实现处理任务也可以。SDWebImage中的自定义operation就只实现了start方法。

  • isConcurrent是否是并发,返回YES即可。

  • isExecuting:是否正在执行

  • isFinished 是否完成,包括任务完成和取消,取消也是Finished

我自己没有实现过自定义NSOperation,之前看SDWebImage的时候看过里面自定义SDWebImageDownloaderOperation类实现了异步处理图片下载请求,写了很多东西,要控制各种状态,并自行手动实现KVO(因为以上几种状态都是readonly),感兴趣的可以参考SDWebImage。

添加任务依赖

NSOperation可以指定任务间的依赖,一个任务A可以依赖于另外一个任务B,任务B没有执行完成,任务A不会开始执行。这里要注意的是,任务B不一定非要是执行成功,因为取消也算是完成isFinished

[operation2 addDependency:operaion1];

添加依赖一定要在operation 执行start方法或者添加到NSOperationQueue前,否则无效。

而且这两个operation不一定非要是在同一个NSOperationQueue中,不同的NSOperationQueue中的operation也可以指定依赖。

- (void)blockOperationAction {
    NSBlockOperation *operaion1 = [NSBlockOperation blockOperationWithBlock:^{
        sleep(2);
        NSLog(@"operation1");
        NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
    }];
    
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation2");
        NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
    }];
    
    [operation2 addDependency:operaion1];

    NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
    [queue1 addOperation:operaion1];
    
    NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
    [queue2 addOperation:operation2];
}

打印如下:

2019-01-01 12:29:44.008728+0800 XXX[11812:1350215] operation1
2019-01-01 12:29:44.009073+0800 XXX[11812:1350215] operation1 Thread = {number = 3, name = (null)}
2019-01-01 12:29:44.009623+0800 XXX[11812:1350214] operation2
2019-01-01 12:29:44.009877+0800 XXX[11812:1350214] operation2 Thread = {number = 4, name = (null)}

NSOperation还有很多功能:

  • 可以通过queuePriority属性指定在队列中的优先级;

  • 可以通过setCompletionBlock:方法设置任务完成的回调;

  • 可以手动cancel取消掉一个任务;也可以通过调用NSOperationQueuecancelAllOperations方法取消队列中所有的任务。

  • 可以通过NSOperationQueuesuspended来暂停队列中的任务。不过它只是暂停operation queue调度新的任务,并不会暂停正在执行的任务。

  • 还可以通过NSOperationQueuemaxConcurrentOperationCount属性来设置operation queue中任务的最大并发数。如果设置为1那就是串行执行。但是这里的串行执行顺序并不一定,要看当然operation的优先级和isReady的状态。

NSOperation提供的对于任务的控制很丰富很灵活,可以做很多事情。

你可能感兴趣的:(Objective-C-(四)-多线程)