美文网首页
iOS 多线程的实现

iOS 多线程的实现

作者: 洁简 | 来源:发表于2017-03-22 22:40 被阅读53次

    这里自己总结自己了解过的用过的一些多线程的方案,以及一些使用方法

    进程和线程

    说到多线程,往往在面试的时候会问一下进程和线程的区别,首先来说说进程,我们可以说进程是指在系统中正在运行的一个应用程序,也可以说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。而线程是进程的一个实体,是CPU调度和分派的基本单位,它使比进程更小的能独立运行的基本单位。简而言之,线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟大就在于它少之又少的系统开销。虽然这些答案都可以在面试题的答案找到,但是当我们叙述给一个不是做编程的人解释时还是很难让人理解,这让我想起家里人经常问我软件是什么,我说了一些自己的概念家里人也只是似懂非懂的感觉,这样不如我们可以举例子说明,比如打开QQ,微博他们就可以说是一个进程,而当我们用QQ聊天,听音乐都可以说是在线程中进行的。

    多线程

    说完线程和进程,再说到多线程,我们知道当进入程序都会有一个主线程,我们也会称为UI线程,我们往往会在这里进行UI更新操作(可以参考:刷新UI为什么在主线程里),而当有一些耗时操作我们往往不会在这里进行,这时我们就要开启多个线程

    每条线程可以并行(同时)执行不同的任务,然而同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象,如果线程非常非常多,CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)

    多线程的作用:
    1.网络请求 2.图片加载 3.文件处理 4.数据存储 5.任务执行
    多线程的优缺点:
    优点:1.简化编程模型 2.更加轻量级 3.提高执行效率 4.提高资源利用率
    缺点:1.增加程序设计复杂行 2.占用内存空间 3.增加CPU调度开销
    iOS在多线程中有4种方案
    1.pThread 2.NSThread 3.GCD(GrandCentral Dispatch) 4.NSOperation

    pThread

    这个方案之前都不知道,最近才看到的,应该很少人会用它,但看他的介绍在很多操作系统都可以用,移植性很强,但他是基于C语言的,用起来也很别扭,简单看一下他在iOS中的使用
    首先需要导入#import <pthread.h>头文件然后创建线程

    - (void)pThreadClick {
        NSLog(@"主线程");
        //极少用
        pthread_t pthread;
        pthread_create(&pthread, NULL, run, NULL);
    }
    void *run(void *data) {
        NSLog(@"子线程");
        for (int i = 1; i<10; i++) {
            NSLog(@"%d",i);
            sleep(1);
        }
        return NULL;
    }
    

    打印可见

    2017-03-22 17:37:25.928 CJXProject[761:16992] 主线程
    2017-03-22 17:37:25.980 CJXProject[761:17371] 子线程
    2017-03-22 17:37:25.999 CJXProject[761:17371] 1
    2017-03-22 17:37:27.111 CJXProject[761:17371] 2
    2017-03-22 17:37:28.120 CJXProject[761:17371] 3
    

    由于几乎用不到就简单看一下他的创建就好了

    NSThread

    这个方案是由苹果封装的,写起来也很顺眼,但是他的生命周期需要手动管理,我们往往会在调试的时候简单使用,有兴趣的可以先看一下官方文档:NSThreadClass Reference
    再看看如何创建使用的,他的创建有三种方式
    1.直接init创建

    //通过init方式创建
    NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(runThread) object:nil];
    [thread1 start];
    

    这种方式需要手动启动
    2.通过detachNewThreadSelector创建并且自动启动

        //通过detachNewThreadSelector
        //[NSThread detachNewThreadSelector:@selector(runThread) toTarget:self withObject:nil];
    

    3.通过performSelectorInBackground创建并且自动启动

        //通过performSelectorInBackground
        //[self performSelectorInBackground:@selector(runThread) withObject:nil];
    

    另外我们可以给线程起名字,可以取消

    [thread1 setName:@"线程1"];//设置线程名字
    [thread1 cancel];//取消
    

    如果创建多个还可以设置优先级

    [thread1 setThreadPriority:0.7];//优先级
    

    其实这种方式我们用的时候也不多往往会在测试的时候用。

    GCD

    Grand Central Dispatch (GCD),它是为苹果多核的并行运算提出的解决方案,所以会自动合理的利用更多的CPU内核,更重要的是它会自动的管理线程的生命周期(创建线程,调度任务,销毁线程)。更重要的是,GCD是Apple官方推荐的方式,所以很多第三方组件都是用GCD来实现的,应用使用GCD有时候就可以和第三方组件很好地配合,同时它使用的也是 C语言,不过由于使用了 Block,使得使用起来更加方便,而且灵活。
    在开始使用GCD的时候,需要搞清楚任务和队列这两个概念。
    任务有两种执行方式:
    1.同步操作(sync),它会阻塞当前线程的操作并等待Block中的任务执行完毕,然后当前线程才会继续往下执行。
    2.异步操作(async),当前线程会直接的往下执行,不会阻塞当前的线程。
    队列也有两种队列,串行队列与并行队列
    串行队列:遵照先进先出的原则,取出来一个执行一个。
    并行队列:也会遵照先进先出的原则,但不同的是它会将取出来的任务放到别的线程执行,然后再取出来一个放到另一个线程。
    在GCD中还有一个特殊的队列———主队列,用来执行主线程上的操作,dispatch_get_main_queue() 它是全局可用的串行队列.
    先看一个全局并行队列:

    //define DISPATCH_QUEUE_PRIORITY_HIGH 2 优先级最高
    //define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 优先级中等
    //define DISPATCH_QUEUE_PRIORITY_LOW (-2) 优先级最低
        NSLog(@"主线程");
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"子线程");
            sleep(2);
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"主线程");
            });
        });
    

    这是系统提供的一个并发队列。
    我们也可以自己创建队列:

    //其中第一个参数是标识符
    //第二个参数用于表示创建的队列是串行还是并行的,传入DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。DISPATCH_QUEUE_CONCURRENT表示创建并行队列
    dispatch_queue_t queue = dispatch_queue_create("gcd", DISPATCH_QUEUE_CONCURRENT);
    

    然后就可以创建任务:

    dispatch_async(queue, ^{
            NSLog(@"1");
            sleep(2);
        });
    

    另外dispatch_group_async(队列组)的使用,队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过dispatch_group_notify()方法获得完成通知:

    //1.创建队列
        dispatch_queue_t queue = dispatch_queue_create("gcd", DISPATCH_QUEUE_CONCURRENT);
    //2.创建队列组
        dispatch_group_t group = dispatch_group_create();
    //执行任务
    dispatch_group_async(group, queue, ^{
            NSLog(@"start game1");
            [NSThread sleepForTimeInterval:2];
            NSLog(@"end game1");
        });
        dispatch_group_async(group, queue, ^{
            NSLog(@"start game2");
            [NSThread sleepForTimeInterval:2];
            NSLog(@"end game2");
        });
        dispatch_group_notify(group, queue, ^{
            NSLog(@"game over");
        });
    

    打印结果:

    2017-03-22 20:45:21.514 CJXProject[2261:58481] start game1
    2017-03-22 20:45:21.515 CJXProject[2261:58650] start game2
    2017-03-22 20:45:23.571 CJXProject[2261:58481] end game1
    2017-03-22 20:45:23.571 CJXProject[2261:58650] end game2
    2017-03-22 20:45:23.574 CJXProject[2261:58650] game over
    

    可以看到如我们预期的结果
    如果我们有两个网络异步请求,当完成后需要得到通知我们就可以用到这个方法下面就做一下:

    //模拟网络异步请求
    - (void)sendMsg1:(void(^)())block {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"start game1");
            [NSThread sleepForTimeInterval:2];
            NSLog(@"end game1");
            dispatch_async(dispatch_get_main_queue(), ^{
                if (block) {
                    block();
                }
            });
        });
    }
    - (void)sendMsg2:(void(^)())block {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"start game2");
            [NSThread sleepForTimeInterval:2];
            NSLog(@"end game2");
            dispatch_async(dispatch_get_main_queue(), ^{
                if (block) {
                    block();
                }
            });
        });
    }
    

    然后利用上面方法获得通知:

        dispatch_group_async(group, queue, ^{
            [self sendMsg1:^{
                NSLog(@"game1 done");
            }];
        });
        dispatch_group_async(group, queue, ^{
            [self sendMsg2:^{
                NSLog(@"game2 done");
            }];
        });
        dispatch_group_notify(group, queue, ^{
            NSLog(@"game over");
        });
    
    

    打印结果如下:

    2017-03-22 20:59:00.950 CJXProject[2400:62715] start game1
    2017-03-22 20:59:00.950 CJXProject[2400:62718] start game2
    2017-03-22 20:59:00.956 CJXProject[2400:62714] game over
    2017-03-22 20:59:03.002 CJXProject[2400:62715] end game1
    2017-03-22 20:59:03.002 CJXProject[2400:62718] end game2
    2017-03-22 20:59:03.005 CJXProject[2400:62605] game1 done
    2017-03-22 20:59:03.007 CJXProject[2400:62605] game2 done
    

    我们会发现和我们预期的不太一样,当网络请求还没有完成我们就得到了通知,这是因为这两个请求都是异步请求,当我们调用第一个请求的时候就是异步请求,所以她很快就完成了就不在持有这个请求了,这时我们就可以用dispatch_group_enter()来完成我们的需求:

        dispatch_group_enter(group);
        [self sendMsg1:^{
            NSLog(@"game1 done");
            dispatch_group_leave(group);
        }];
        dispatch_group_enter(group);
        [self sendMsg2:^{
            NSLog(@"game2 done");
            dispatch_group_leave(group);
        }];
    

    这时打印如下:

    2017-03-22 21:07:43.803 CJXProject[2486:65358] start game1
    2017-03-22 21:07:43.805 CJXProject[2486:65650] start game2
    2017-03-22 21:07:45.817 CJXProject[2486:65358] end game1
    2017-03-22 21:07:45.817 CJXProject[2486:65650] end game2
    2017-03-22 21:07:45.820 CJXProject[2486:65268] game1 done
    2017-03-22 21:07:45.823 CJXProject[2486:65268] game2 done
    2017-03-22 21:07:45.826 CJXProject[2486:65650] game over
    

    这样就可以满足我们的需求了。
    另外如果我们只想让一段代码执行一次,我们可以用dispatch_once(),我们还会常常在单例的时候用到它:

    + (instancetype)instance {
        static dispatch_once_t onceToken;
        static CJXSingle *single = nil;
        dispatch_once(&onceToken, ^{
            NSLog(@"init CJXSingle");
            single = [[CJXSingle alloc]init];
        });
        return single;
    }
    

    上面就是一个常见单例的写法,以后会单独总结一下单例,这里就提一下。
    另外还有一个常用的dispatch_after()也就是延迟执行简单实现一下:

        NSLog(@"开始");
    //延迟2秒
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"结束");
        });
    

    还有之前写过的dispatch_source来创建计时器
    另外还有不是很常用的:
    dispatch_barrier_async():写入操作会确保队列前面的操作执行完毕才开始,并会阻塞队列中后来的操作.直到它执行完成后才会执行。
    dispatch_apply():重复执行某个任务。

    NSOperation

    NSOperation 是苹果公司对 GCD 的封装,NSOperation 只是一个抽象类,不能用于封装任务, 所以需要用它的子类NSInvocationOperation 和 NSBlockOperation来封装,两种方式没有本质的区别,但是后者使用Block的形式进行代码组织,在使用的过程中更加方便。
    可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。
    操作步骤也很好理解:
    1.将要执行的任务封装到一个 NSOperation 对象中。
    2.将此任务添加到一个 NSOperationQueue 对列中,线程就会依次启动
    先看一下NSInvocationOperation的创建:

        NSInvocationOperation *invocation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(invocationClick) object:nil];
        [invocation start];
    

    在看NSBlockOperation:

    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
            
        }];
        [blockOperation start];
    

    正如我们上面所说都差不多,但是这样的任务,默认会在当前线程执行。这时我们就可以可以创建一个NSOperationQueue,然后将加入任务加入:

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc]init];
    [self.operQueue addOperation:blockOperation];
    

    相比NSInvocationOperation推荐使用NSBlockOperation,因为它代码简单,同时由于闭包性使它没有传参问题,NSInvocationOperation在Swift中已不再支持。
    另外我们可以自定义NSOperation,实现-(void)main函数,新开一个线程,下面就简单实现一下:
    首先新建一个文件并且继承与NSOperation,然后重写一下main方法:

    - (instancetype)initWithName:(NSString *)name {
        if (self = [super init]) {
            self.operName = name;
        }
        return self;
    }
    - (void)main {
        for (int i = 0; i < 3; i++) {
            NSLog(@"%@ %d",self.operName,i);
            sleep(1);
        }
    }
    

    怎么使用呢看下面:

        CJXOperation *cjxOper1 = [[CJXOperation alloc]initWithName:@"oper1"];
        CJXOperation *cjxOper2 = [[CJXOperation alloc]initWithName:@"oper2"];
        CJXOperation *cjxOper3 = [[CJXOperation alloc]initWithName:@"oper3"];
        [self.operQueue addOperation:cjxOper1];
        [self.operQueue addOperation:cjxOper2];
        [self.operQueue addOperation:cjxOper3];
    

    这样就可以了,另外我们可以设置最大并发数:
    [self.operQueue setMaxConcurrentOperationCount:3];表示最多3个
    另外当NSOperation对象需要依赖于其它NSOperation对象完成时再操作,就可以通过addDependency方法添加一个或者多个依赖的对象,只有所有依赖的对象都已经完成操作后,最开始的NSOperation对象才会开始执行,通过removeDependency来删除依赖对象。

        //依赖
        [cjxOper3 addDependency:cjxOper2];
        [cjxOper2 addDependency:cjxOper1];
    

    打印如下:

    2017-03-22 21:55:10.609 CJXProject[2486:81331] oper1 0
    2017-03-22 21:55:11.646 CJXProject[2486:81331] oper1 1
    2017-03-22 21:55:12.714 CJXProject[2486:81331] oper1 2
    2017-03-22 21:55:13.716 CJXProject[2486:81331] oper2 0
    2017-03-22 21:55:14.755 CJXProject[2486:81331] oper2 1
    2017-03-22 21:55:15.828 CJXProject[2486:81331] oper2 2
    2017-03-22 21:55:16.897 CJXProject[2486:81331] oper3 0
    2017-03-22 21:55:17.962 CJXProject[2486:81331] oper3 1
    2017-03-22 21:55:18.978 CJXProject[2486:81331] oper3 2
    

    可以看到3依赖2,2依赖1,注意:不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。
    大体上的用法差不多都讲完了,但是实际中的运用还是需要自己多实践,最后总结一下:
    pThread极少使用
    NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制.
    NSOperation进行多线程开发可以控制线程总数及线程依赖关系.
    相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题.
    NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD.
    在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效.
    在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行).
    另外还有一些同步中加锁的方法如:
    @synchronized(self) { //需要执行的代码块 }
    另外关于线程锁可以参考多线程开发之线程安全篇
    这样基本上就把多线程用到的方案都总结了一下,也借鉴了不少别的博客的东西,如果想更加深入的学习可以看看一些大牛的总结,或者去看看官方文档。
    可以参考关于iOS多线程,你看我就够了
    iOS多线程总结 以及这篇文章下面的参考文章讲的比较深,可以让我们更好的了解多线程。

    相关文章

      网友评论

          本文标题:iOS 多线程的实现

          本文链接:https://www.haomeiwen.com/subject/refnnttx.html