• iOS开发-多线程编程技术(Thread、Cocoa operations、GCD)


    简介

    在软件开发中,多线程编程技术被广泛应用,相信多线程任务对我们来说已经不再陌生了。有了多线程技术,我们可以同做多个事情,而不是一个一个任务地进行。比如:前端和后台作交互、大任务(需要耗费一定的时间和资源)等等。也就是说,我们可以使用线程把占据时间长的任务放到后台中处理,而不影响到用户的使用。

    线程间通讯

    有一个非常重要的队列,就是主队列。在这个队列中处理多点触控及所有与UI相关操作等等。它非常特殊,原因有两点。一是我们绝对不想它阻塞,我们不会将需要执行很长时间的任务放在主队列上执行。二是我们将其用于所有与UI相关的同步,也就是线程间通讯需要注意的地方。所有有可能会使屏幕UI发生变化的,都应放在主队列上执行。

    线程的定义:

    每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。

    转自百度百科:多线程

    如果熟悉多线程编程技术这一块的朋友们,可以去看关于多线程安全的文章,是我写的另一篇文章”iOS开发-多线程开发之线程安全篇“;

    IOS支持的多线程技术:

    一、Thread:

    1)显式创建线程:NSThreed

    2)隐式创建线程:NSObject

    二、Cocoa operations:

    NSOperation类是一个抽象类,因为我们必须使用它的两个子类。

      1)NSInvocationOperation 

    2)NSBlockOperation

    ————————————————————————————

    3)NSOperationQueue(继承于NSObject)

    三、Grand Central Dispatch (GCD):

    1)GCD的创建

    2)重复执行线程及一次性执行:dispatch_apply & dispatch_once

    3)操作(串行)队列:dispatch_queue_create

    4)GCD群组通知:dispatch_group_t

    5)GCD实现计时器

    6)后台运行

    7)延迟执行

    四、比较多线程技术

    一、Thread

    我们可以使用NSTherad或NSObject类去调用:

    1)显式创建线程:NSThread

    创建NSThread有两个办法

    1.1)创建之后需要使用start方法,才会执行方法:

    NSThread *threadAlloc = [[NSThread alloc] initWithTarget:self selector:@selector(threadAlloc) object:nil];
    [threadAlloc start];

    1.2)创建并马上执行方法:

    [NSThread detachNewThreadSelector:@selector(threadAlloc:) toTarget:self withObject:nil];

    2)隐式创建线程:NSObject

    我们也可以使用NSObject类的方法直接调用方法

    [self performSelectorInBackground:@selector(threadAlloc) withObject:nil];

    取消线程的方法:

    实际上并没有真正提供取消线程的API。苹果提供了一个cancel的api,但它不能作用于取消线程,它只能改变线程的运行状态。我们可以使用它来进行条件判断。

    - (void)threadCancel
    {
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadCancelNow) object:nil];
        [thread start];
    }
    
    - (void)threadCancelNow
    {
        int a = 0;
        while (![[NSThread currentThread] isCancelled]) {
            NSLog(@"a - %d", a);
            a++;
            if (a == 5000) {
                NSLog(@"终止循环");
                [[NSThread currentThread] cancel];
                break;
            }
        }
    }

    程序效果:循环输出5000次,线程就会被终止。

    NSThread线程间通讯-调用主线程修改UI:

    只需要传递一个selector和它的参数,withObject参数可以为nil,waitUntilDone代表是否要等待调用它的这个线程执行之后再将它从主队列调出,并在主队列上运行,通常设为NO,不需要等待。

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; 

    NSThread相关属性及方法:

    // 获取/设置线程的名字
    @property (copy) NSString *name NS_AVAILABLE(10_5, 2_0);
    
    /**
     *  获取当前线程的线程对象
     *
     *  通过这个属性可以查看当前线程是第几条线程,主线程为1。
     *  可以看到当前线程的序号及名字,主线程的序号为1,依次叠加。
     */
    + (NSThread *)currentThread;
    
    // 线程休眠(秒)
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;
    
    // 线程休眠,指定具体什么时间休眠
    + (void)sleepUntilDate:(NSDate *)date;
    
    // 退出线程 
    // 注意:这里会把线程对象销毁!销毁后就不能再次启动线程,否则程序会崩溃。
    + (void)exit;

    二、Cocoa operations

    1)NSInvocationOperation

    创建NSInvocationOperation线程,附带一个NSString参数:

    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"abc"];
    // 需要启动线程,默认是不启动的。
    [operation start];

    如在创建时定义了参数,那么接收的时候,可以对sender进行转换,如字符串、数组等:

    - (void)invocationAction:(NSInvocationOperation *)sender
    {
        NSLog(@"sender - %@", sender);      // 输出params
        NSString *str = (NSString *)sender;
        NSLog(@"str - %@e", str);           // params
    }

    附带一提,线程的普通创建一般为并发执行的,因为串行队列是需要显式创建的,如没看见此类代码,那么即是并发队列线程,因此,上述代码也就是并发线程。关于并发和串行队列(线程),我将会在下面详细说明,我们继续往下看。

    你也可以使用NSOperationQueue来创建一个线程队列,用来添加子线程:

    NSOperationQueue *invocationQueue = [[NSOperationQueue alloc] init];
    
    // 线程A
    NSInvocationOperation *invocationQ1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"invocationQ1"];
    
    // 线程B
    NSInvocationOperation *invocationQ2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction:) object:@"invocationQ2"];
    
    // 往invocationQueue添加子线程
    [invocationQueue addOperations:@[invocationQ1, invocationQ2] waitUntilFinished:YES];

    必须使用addOperations:方法把线程添加至队列,不然线程不会执行,队列是并行执行。或者,你也可以使用addOperation:方法添加单个线程。

    2)NSBlockOperation

    创建NSBlockOperation

    // 创建线程任务
    NSBlockOperation *blockOperation = [NSBlockOperation
                                        blockOperationWithBlock:^{
                                            [NSThread sleepForTimeInterval:2];
                                            NSLog(@"one - %@", [NSThread currentThread]);
                                        }];;// 执行线程任务
    [blockOperation start];

    注意:这会在当前的线程中执行,因为它是根据调用的线程所决定的。

    比方说你在主线程中运行它,那么它就是在主线程中执行任务。如果你是在子线程中运行它,那么它就是在子线程中执行任务。

    做个简单的实验,我们新建一条子线程,然后在子线程里调用NSBlockOperation

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSBlockOperation *blockOperation = [NSBlockOperation
                                            blockOperationWithBlock:^{
                                                [NSThread sleepForTimeInterval:2];
                                                NSLog(@"one - %@", [NSThread currentThread]);
                                                // print: one - <NSThread: 0x7f8ac2e1d0b0>{number = 2, name = (null)}
                                            }];;
        
        [blockOperation start];
    });

    它将打印:one - <NSThread: 0x7f8ac2e1d0b0>{number = 2, name = (null)},因此这个理论是正确的

    我们也可以使它并发执行,通过使用addExecutionBlock方法添加多个Block,这样就能使它在主线程和其它子线程中工作。

    NSBlockOperation *blockOperation = [NSBlockOperation
                                        blockOperationWithBlock:^{
                                            NSLog(@"one - %@", [NSThread currentThread]);
                                        }];;
    
    [blockOperation addExecutionBlock:^{
        NSLog(@"two - %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
        NSLog(@"three - %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
        NSLog(@"four - %@", [NSThread currentThread]);
    }];
    
    [blockOperation start];

    它将打印:

    two - <NSThread: 0x7fea8a70b000>{number = 3, name = (null)}
    one - <NSThread: 0x7fea8a558a40>{number = 4, name = (null)}
    four - <NSThread: 0x7fea8a406b90>{number = 1, name = main}
    three - <NSThread: 0x7fea8a436e40>{number = 2, name = (null)}

    大家都看到,即使我们通过使用addExecutionBlock方法使它并发执行任务,但是它也依旧会在主线程执行,因此我们就需要使用NSOperationQueue了。

    3)NSOperationQueue

    这里介绍一下NSOperation的依赖关系,依赖关系会影响线程的执行顺序:

    // 创建操作队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 线程A
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"op1");
        [NSThread sleepForTimeInterval:2];
    }];
    
    // 线程B
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"op2");
    }];
    
    // 线程C
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"op3");
        [NSThread sleepForTimeInterval:2];
    }];
    
    // 线程B依赖线程C,也就是等线程C执行完之后才会执行线程B
    [op2 addDependency:op3];
    // 线程C依赖线程A,同上,只不过依赖对象改成了线程A
    [op3 addDependency:op1];
    
    // 为队列添加线程
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];

    当你没添加依赖时,队列是并行执行的。

    注意:依赖关系可以多重依赖,但不要建立循环依赖。

    Cocoa operations线程间通信-调用主线程修改UI:

    // 创建线程对象(并发)
    NSBlockOperation *blockOperation = [[NSBlockOperation alloc] init];
    
    // 添加新的操作
    [blockOperation addExecutionBlock:^{
        NSLog(@"two - %@", [NSThread currentThread]);
    }];
    
    // 添加新的操作
    [blockOperation addExecutionBlock:^{
        NSLog(@"three - %@", [NSThread currentThread]);
        // 在主线程修改UI
        NSOperationQueue *queue = [NSOperationQueue mainQueue];
        [queue addOperationWithBlock:^{
            [self editUINow];
        }];
    }];
    
    [blockOperation start];

     NSOperation方法及属性:

    // 设置线程的最大并发数
    @property NSInteger maxConcurrentOperationCount;
    
    // 线程完成后调用的Block
    @property (copy) void (^completionBlock)(void);
    
    // 取消线程
    - (void)cancel;

    只列举上面那些,其它的方法就不全列出来了。

    注意:在NSOperationQueue类中,我们可以使用cancelAllOperations方法取消所有的线程。这里需要说明一下,不是执行cancelAllOperations方法时就会马上取消,是等当前队列执行完,下面的队列不会再执行。

    三、Grand Central Dispatch (GCD)

    1)GCD异步线程的创建:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程 - %@", [NSThread currentThread]);
    });

    GCD也可以创建同步的线程,只需要把async改成sync即可。

    2)重复执行线程:dispatch_apply

    以下代码会执行4次:

    dispatch_apply(4, DISPATCH_QUEUE_PRIORITY_DEFAULT, ^(size_t index) {
        // index则为执行的次数 0开始递增
        NSLog(@"one - %ld", index);
    });

    index参数为执行的次数,从0开始递增。

    其中需要注意的是,每次执行都会新开辟一条子线程,因为是异步的原因,它们不会是顺序的。

    [657:159159] one - 0, thread - <NSThread: 0x100110b50>{number = 1, name = main}
    [657:159191] one - 2, thread - <NSThread: 0x103800000>{number = 2, name = (null)}
    [657:159192] one - 3, thread - <NSThread: 0x100112b90>{number = 3, name = (null)}
    [657:159190] one - 1, thread - <NSThread: 0x100501180>{number = 4, name = (null)}

    然而,GCD还有一次性执行的方法:

    dispatch_once_t once;
    dispatch_once(&once, ^{
        NSLog(@"once - %@", [NSThread currentThread]); // 主线程
    });

    它通常用于创建单例。

    3)操作队列:dispatch_queue_create

    使用GCD也能创建串行队列,具体代码如下:

    /**
     *  GCD创建串行队列
     *
     *  @param "com.GarveyCalvin.queue"  队列字符串标识
     *  @param DISPATCH_QUEUE_CONCURRENT 可选的,可以是NULL
     *
     *  @return dispatch_queue_t
     */
    dispatch_queue_t queue = dispatch_queue_create("com.GarveyCalvin.queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 线程A
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"sleep async - %@", [NSThread currentThread]);
    });
    
    // 线程B
    dispatch_barrier_async(queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"sleep barrier2 - %@", [NSThread currentThread]);
    });
    
    // 线程C
    dispatch_async(queue, ^{
        NSLog(@"async");
    });

    运行效果:以上会先执行 线程A-》线程B-》线程C,它是一个串行队列。

    dispatch_queue_create的第二个参数:

    1)DISPATCH_QUEUE_SERIAL(串行)

    2)DISPATCH_QUEUE_CONCURRENT(并发)

    4)GCD群组通知:dispatch_group_t

    GCD的高级用法,等所有线程都完成工作后,再作通知。

    // 创建群组
    dispatch_group_t group = dispatch_group_create();
    
    // 线程A
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"group1");
        [NSThread sleepForTimeInterval:2];
    });
    
    // 线程B
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"group2");
    });
    
    // 待群组里的线程都完成之后调用的通知
    dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"group success");
    });

    群组里的线程也是并行队列。线程A和线程B都执行完之后,会调用通知打印group success。

    5)GCD实现计时器

    __block int time = 30;
    CGFloat reSecond = 1.0;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, reSecond * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(timer, ^{
        time--;
        NSLog(@"%d", time);
        if (time == 0) {
            dispatch_source_cancel(timer);
        }
    });
    dispatch_resume(timer);

    代码效果:创建了一个计时器,计时器运行30秒,每过一秒会调用一次block,我们可以在block里面写代码。因为dispatch_source_t默认是挂起状态,因此我们使用时需要使用dispatch_resume方法先恢复,不然线程不会执行。

    GCD线程间通信-调用主线程修改UI:

    有时候我们请求后台作数据处理,数据处理是异步的,数据处理完成后需要更新UI,这时候我们需要切换到主线程修改UI,例子如下:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"异步数据处理 - %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"数据处理完成");
        
        // 调用主线程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"更新UI - %@", [NSThread currentThread]);
            [self editUINow];
        });
    });

    因为是在主线程修改UI,所以我们最好是使用同步的GCD方法dispatch_sync。但这还不够,我们还需要使用dispatch_get_main_queue()方法来获得主线程,之后就是作UI的更新工作了。

    GCD方法及属性:

    // 获取主线程
    dispatch_get_main_queue()
    
    // 创建队列:第一个参数是队列的名称,它会出现在调试程序等之中,是个内部名称。第二个参数代表它是串行队列还是并并发队列,NULL代表串行队列。
    dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
    
    // 创建异步调度队列
    void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
    
    // 恢复队列
    void dispatch_resume(dispatch_object_t object);
    
    // 暂停队列
    void dispatch_suspend(dispatch_object_t object);

    小结:本文主要介绍了IOS三种线程对比及其使用方法。需要特别注意的是,在修改任何有关于UI的东西,我们必须要切换至主线程,在主线程里修改UI,避免不必要的麻烦产生。苹果是推荐我们使用GCD,因为GCD是这三种里面抽象级最高的,使用起来也简单,也是消耗资源最低的,并且它执行效率比其它两种都高。因此,能够使用GCD的地方,尽量使用GCD。

    6)后台运行

    使用block的另一个好处是可以让程序在后台较久地运行。在以前,当应用被按Home键退出后,应用仅有最多5秒的时间做一些保存或清理资源的工作。 但是如果使用GCD,你可以让你的应用最多有10分钟的时间在后台长久运行。这个时间可以用来做各种事情,包括清理本地缓存、发送统计数据等工作。

    AppDelegate.h
    @interface AppDelegate ()
    
    @property (assign, nonatomic) UIBackgroundTaskIdentifier backGroundUpdate;
    
    @end
    
    AppDelegate.m
    - (void)applicationDidEnterBackground:(UIApplication *)application {
        [self beginBackGroundUpdate];
       // 需要长久运行的代码
        [self endBackGroundUpdate];
    }
    
    - (void)beginBackGroundUpdate
    {
        self.backGroundUpdate = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            [self endBackGroundUpdate];
        }];
    }
    
    - (void)endBackGroundUpdate
    {
        [[UIApplication sharedApplication] endBackgroundTask:self.backGroundUpdate];
        self.backGroundUpdate = UIBackgroundTaskInvalid;
    }

    建议大家在真机上测试,因为笔者在模拟器测试了24分钟还有效。

    7)延迟执行

    如果我们想要某段代码延迟执行,那么可以使用dispatch_after ,但是有一个缺点是,当提交代码后(代码执行后),我们不能取消它,它将会运行。另外,我们可以使用 NSTimer 进行延时操作,值得一提,它是可以被取消的。

    dispatch_time_t time_t = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC));
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after(time_t, queue, ^{
        NSLog(@"hahalo");
    });

    比较多线程技术

    一、Thread: 

    优点:量级较轻。

    缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。

    二、Cocoa operations:

    优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。

    三、Grand Central Dispatch (GCD):

    优点:GCD基于C的API,非常底层,可以充分利用多核,能够轻松在多核系统上高效运行并发代码,也是苹果推荐使用的多线程技术。

    本文参考:

    iOS多线程开发

    GCD的另一个用处是可以让程序在后台较长久的运行。

    全面掌握iOS多线程攻略 —— PS:这个攻略较多,但是有很多重复的内容。

    iOS多线程的初步研究(一)-- NSThread


    博文作者:GarveyCalvin

    博文出处:http://www.cnblogs.com/GarveyCalvin/

    本文版权归作者和博客园共有,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作!

  • 相关阅读:
    java ElasticSearch并发操作之乐观锁的使用
    ES并发更新导致冲突的问题
    012 found ' ' @ line 30, column 50. 'com.google.code.gson:gson:2.7
    011 Error:Failed to resolve: com.android.support:recyclerview-v7:28.0.0解决方法
    010 Failed to resolve: com.squareup.okhttp3:okhttp:3.4.1
    009 A problem occurred configuring project ':app'. > Failed to notify project evaluation listener. >
    008 导入项目每次都要Gradle:Download https://services.gradle.org/distributions/gradle
    007 Android Studio错误Error:(23, 17) 解决方案
    006 Unable to tunnel through proxy. Proxy returns "HTTP/1.1 400 Bad Request"解决办法
    005 Android Studio 快捷键大全
  • 原文地址:https://www.cnblogs.com/GarveyCalvin/p/4206009.html
Copyright © 2020-2023  润新知