69

iOS开发进阶:多线程

 5 years ago
source link: https://jesuslove.github.io/2018/09/26/iOS开发进阶:多线程/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

NFR32ei.png!web

首先看一道面试题

iOS中多线程有哪些实现方案?

技术方案 简介 语言 线程声明周期 使用频率 pthread 1. 一套通用的多线程API
2. 跨平台/可移植
3. 使用难度大 C 程序员管理 几乎不用 NSThread 1.面向对象
2.简单易用直接操作线程对象 OC 程序员管理 偶尔使用 GCD 1.旨在替代NSThread等线程技术
2.充分利用设备的多核 C 自动管理 经常使用 NSOperation 1.基于GCD
2.比GCD多了一些更实用的函数 OC 自动管理 经常使用

iOS中,多线程一般有三种方案 GCDNSOperationNSThread

这一篇文章,会学习iOS中关于多线程相关的问题,以及面试中的问题。

一、GCD

GCD相关问题一般分为三个方面:首先, 同步/异步串行/并发 问题;其次, dispatch_barrier_async 异步栅栏调用,解决多读单写问题;最后, dispatch_group 使用和理解。

GCD中有2中用来执行任务的函数:同步和异步;同时还有两种类型的队列:并发和串行队列。并发队列让多个任务并发执行,自动开启多个线程同时执行任务。并发功能只在异步函数下才生效。

并发队列 手动创建的串行队列 主队列 同步 没有开辟新线程
串行执行 没有开辟新线程
串行执行 没有开辟新线程
串行执行 异步 有开辟新线程
并发执行 有开辟新线程
串行执行 没有开辟新线程
串行执行

注意:使用同步函数往当前串行队列中添加任务,会卡主当前的串行队列,产生死锁。

1.1 同步/异步串行/并发

存在四种组合方案:

// 同步 + 串行
dispatch_sync(serial_queue, ^{
//任务
});
// 异步 + 串行
dispatch_async(serial_queue, ^{
//任务
});
// 同步 + 并发
dispatch_sync(concurrent_queue, ^{
//任务
});
// 异步 + 并发
dispatch_async(concurrent_queue, ^{
//任务
});

1.1.1 同步 + 串行

首先看一道面试题

- (void)viewDidLoad {
  dispatch_sync(dispatch_get_main_queue(), ^{
       [self doSomething];
  });
}

上面这道面试题,存在什么问题?

产生死锁。队列引起循环等待。因为, viewDidLoad() 进入主队列,执行过程中会将 block 添加到主队列中。 viewDidLoad() 需要等待 block 执行完成后才能结束,由于主队列先进先出的 block 需要 viewDidLoad() 执行完毕才能执行。因此导致队列循环等待的问题。

上面的问题理解,在来看一个问题。

- (void)viewDidLoad {
  dispatch_sync(serial_queue, ^{
       [self doSomething];
  });
}

上面这道题,有什么问题?

没有问题。这里是将 block 添加到单独的串行队列。 viewDidLoad() 在主队列中在主线程中执行,在其执行过程中调用 block 添加到串行队列中,在主线程中执行。 同步方式提交任务,无论在串行队列还是并发队列都会在当前线程中执行

1.1.2 同步 + 并发

问题

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        NSLog(@"1");
        dispatch_sync(global_queue, ^{
            NSLog(@"2");
            dispatch_sync(global_queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }
    return 0;
}

上面这段代码的输出结果?

输出结果:12345。同步方式提交任务,无论在串行队列还是并发队列都会在当前线程中执行。

思考,如果换成串行队列呢?

1.1.3 异步 + 串行

略…

1.1.4 异步 + 并发

面试题

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(global_queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
        NSLog(@"3");
    });
}
- (void)printLog {
    NSLog(@"2");
}

上述代码执行的结果?

结果:13。提交异步任务到并发队列,任务调用了 performSelector:withObject:afterDelay: 。由于GCD提交的任务是在某个子线程中执行,子线程没有RunLoop。由于 performSelector:withObject:afterDelay: 需要RunLoop才可生效,所以方法不执行。这个问题,考察 performSelector:withObject:afterDelay: 内部实现。

任务和队列示例代码: 任务和队列Demo包含面试题讲解

二、多读单写解决方案

  • pthread_rwlock: 读写锁
  • dispatch_barrier_async() 异步栅栏调用

怎么利用GCD实现多读单写?或者如何实现多读单写?

2.1 什么是多读单写?

读者和读者,并发。

读者和写者,互斥。

写者与写者,互斥。

2.2 解决方法

dispatch_async(global_queue, ^{
     NSLog(@"读取1");
 });
dispatch_async(global_queue, ^{
     NSLog(@"读取2");
 });
dispatch_barrier_async(global_queue, ^{
     NSLog(@"写入1");
 });
dispatch_async(global_queue, ^{
     NSLog(@"读取3");
 });
dispatch_async(global_queue, ^{
     NSLog(@"读取4");
 });

dispatch_barrier_async 函数会等待追加到并发队列上的并行执行的处理全部结束之后,在将指定的处理追加到该并发队列中。然后等 dispatch_barrier_async 函数追加的处理执行完毕后,并发队列才恢复为一般的动作,追加到并发队列的处理又开始并行执行。

三、dispatch_group_async()

面试题:

如何用GCD 实现:A、B、C三个任务并发,完成后执行任务D?

实现追加到并发队列中的多个任务全部结束后再执行想执行的任务。无论向什么样的队列中追加处理,使用Dispatch Group都可监视这些处理执行的结束。一旦检测到所有处理执行结束,该Dispatch Group与队列相同。

dispatch_group_async()

同dispatch_async()函数相同,都追加Block到指定的Dispatch Queue中。当组中所有任务都执行完成后,dispatch_group_notify()执行Block中的内容。

示例代码:

// dispatch_group_notify
- (void)test {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.lqq.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1 - %@", [NSThread currentThread]);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2 - %@", [NSThread currentThread]);
        }
    });
    //--------------示例1-------------------
    // 写法一, 等上面的任务执行完成后,才会在主队列中执行任务3
//    dispatch_group_notify(group, queue, ^{
//        dispatch_async(dispatch_get_main_queue(), ^{
//            for (int i = 0; i < 5; i++) {
//                NSLog(@"任务3 - %@", [NSThread currentThread]);
//            }
//        });
//    });
    
    //写法二:直接在主队列中执行
//    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任务3 - %@", [NSThread currentThread]);
//        }
//    });
    //--------------示例2-------------------
    // 如果有多个notify会怎么执行呢?
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务3 - %@", [NSThread currentThread]);
        }
    });
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务4 - %@", [NSThread currentThread]);
        }
    });
    // 任务3和任务4是交替执行的
}

另外,也可以使用 dispatch_group_wait ,如下:

// 监控任务是否完成,当完成时会返回0,不完成一直等待。
 long result = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
 if (result == 0) {
     NSLog(@"全部任务执行完成");
 }

线程组示例代码: 线程组API使用Demo

四、NSOperation

需要和NSOperationQueue配合使用来实现多线程。优势和特点:添加任务依赖、任务执行状态控制、控制最大并发量。

任务状态的控制

isReady
isExecuting
isFinished
isCanceled

如果重写main方法,底层控制变更任务执行完成状态,以及任务退出。

如果重写start方法,自行控制任务状态。

面试题

系统是怎样移除一个isFinished=YES的NSOperation的?答案:KVO

五、NSThread

启动流程

start() -> 创建pthread -> main() ->[target performSelector:selector] -> exit()

如何实现常驻进程?

通过使用NSThread和RunLoop实现。

附录

GCD API介绍:
dispatch_queue_create(名称,类型)
类型为NULL时,为串行队列;为DISPATCH_QUEUE_CONCUREENT,为并行队列.。

需要手动release释放队列
dispatch_release(队列)

主队列,串行
dispatch_get_main_queue()
dispatch_get_global_queue(优先级,0)

优先级有四种情况:
    高,默认,低,后台
    
为自己创建的队列设置优先级

dispatch_set_target_queue(自定义队列,其他已知优先级的队列) 

dispatch_after(时间,队列,Block)
时间:dispatch_time_t 

dispatch_group 监听所有任务结束。

dispatch_group_create() //创建一个队列组,需要手动release
dispatch_group_async(组,队列,block)
dispatch_group_notify(组,队列,block) // 所有任务都结束后调用
也可以使用dispatch_group_wait()

dispatch_barrier_async() 一般用于数据库操作和文件读写。

同步任务
dispatch_sync(队列,block) 

异步任务
dispatch_async(队列,block)

指定执行次数
dispatch_apply(次数,队列,block)


挂起
dispatch_suspend(队列)

恢复
dispatch_resume(队列)

信号量
Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。

dispatch_semaphore_create(技术值)
dispatch_semaphore_wait(semaphore, 时间)
dispatch_semaphore_signal(semaphore)
dispatch_reliease(semaphore)

// 示例
// 创建一个对象,默认的计数值为0,等待。当计数值大于1或者等于1时,减去1而不等待。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// 发送计数值加一信号
dispatch_semaphore_signal(semaphore);
// 等待计数值变化,第二个参数是等待时间,这里是永久等待。
// 当计数值大于0时,返回0。等于0不会返回任何值。
long result = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

if (result == 0) {
    NSLog(@"执行排他性操作");
}


dispatch_once函数是保证在应用程序执行中只执行一次的API。


dispatch I/O 分割读取,提高读取效率。

小结

通过几道面试题理解iOS中几种多线程解决方案。笔者认为该课程可以用作知识梳理,将解读的内容不是很全面,其他的内容可以看《小马哥底层原理视频》。

参考文章:

细说 NSOperation

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK