美文网首页iOS Developer
Objective-C高级编程之GCD篇

Objective-C高级编程之GCD篇

作者: 还是不够辣 | 来源:发表于2017-06-05 15:53 被阅读44次

    多线程编程是一项非常重要的技术,目前在iOS开发中比较流行的多线程方案是GCD和NSOperationQueue,本文将详细介绍如何使用GCD进行多线程编程。根据苹果的文档,GCD(Grand Central Dispatch)是一项提供了管理并发和异步执行任务的技术,开发者只需要将想要执行的任务追加到适当的dispatch queue中,GCD会为此生成必要的线程来执行任务。GCD使用非常简洁的语法实现了复杂的多线程编程方案,接下来我们深入认识它。

    dispatch queue

    GCD使用队列来实现多个任务的执行,队列分为两种,serial dispatch queue和concurrent dispatch queue,前者指队列中的任务会使用单一的线程逐个执行,而后者指队列中的任务会使用多个线程并行执行。我们可以自行创建这两种队列,方法如下:

    dispatch_queue_t serialQueue = dispatch_queue_create(@"cn.test.serial.queue", NULL);
    dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    

    dispatch_queue_create函数用于创建一个队列,方法的第一个参数为队列的标签(可为空,但并不推荐这样做),第二个参数指定队列的类型,当指定为NULL时即指serial dispatch queue,指定为DISPATCH_QUEUE_CONCURRENT时为concurrent dispatch queue,需要注意的是iOS6之后,GCD已经纳入了ARC的管理范围,所以不再需要手动调用相关retain/release方法。

    事实上,苹果为我们提供了默认的dispatch queue,供我们使用,因此自行创建dispatch queue并不总是必要的。默认的队列有两种main dispatch queue和global dispatch queue,前者队列中的任务会在主线程中去执行,主线程只有一个,所以它是serial dispatch queue,而后者则属于concurrent dispatch queue,队列中的任务会并发执行,此队列有四种优先级可供选择,分别为DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND ,使用默认队列的方式是这样的:

    //获取main dispatch queue
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    //获取global dispatch queue
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    

    dispatch_get_global_queue函数的第一个参数指定队列优先级,第二个参数默认写0即可

    dispatch_async

    我们使用dispatch_async向某个队列追加一项任务,async表明该方式是异步的,不会阻塞当前线程,使用方式如下

    dispatch_async(serialQueue, ^ {
            NSLog(@"block");
     });
    

    该函数第一个参数指定向哪个队列追加任务,第二个参数是一个block,表示要执行的任务。

    dispatch_set_target_queue

    该函数有两个功能,第一,可以指定某个队列的优先级和目标队列优先级一致,例如

    dispatch_queue_t concurrentQueue = dispatch_queue_create(@"cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_set_target_queue(concurrentQueue, globalQueue);
    

    我们创建的队列都是默认优先级,上述代码表示指定concurrentQueue优先级和globalQueue优先级一致。
    第二,可用于改变队列的执行层次。举个例子,现有A,B, C三个serial dispatch queue,如果指定这三个队列的target queue为D serial dispatch queue,那么原本会并行执行的三个队列在D上就会串行执行,示例如下:

        dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue",NULL);  
        dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);  
        dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);  
        dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);  
          
        dispatch_set_target_queue(queue1, targetQueue);  
        dispatch_set_target_queue(queue2, targetQueue);  
        dispatch_set_target_queue(queue3, targetQueue);  
          
        dispatch_async(queue1, ^{  
            NSLog(@"1 in");  
            [NSThread sleepForTimeInterval:3.f];  
            NSLog(@"1 out");  
        });  
        dispatch_async(queue2, ^{  
            NSLog(@"2 in");  
            [NSThread sleepForTimeInterval:2.f];  
            NSLog(@"2 out");  
        });  
        dispatch_async(queue3, ^{  
            NSLog(@"3 in");  
            [NSThread sleepForTimeInterval:1.f];  
            NSLog(@"3 out");  
        });  
    
    //代码执行结果
     1 in  
     1 out  
     2 in  
     2 out  
     3 in  
     3 out  
    

    dispatch_after

    该函数用于延时调用,例如当我们想要延时3秒执行某项任务时:

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"test");
        });
    

    其中第一个参数是时间,类型为dispatch_time_t,上例中直接使用dispatch_time函数生成,表示从现在开始3秒之后的时间,第二个参数是要在哪个队列中执行,上例为main dispatch queue,第三个参数为要执行的block,该函数实际上是在3秒之后向队列追加了一个block任务。

    dispatch_group

    当我们有多个线程在同时执行任务,我们希望在所有线程中的任务执行完毕后做某项处理,那么就可以将前面的多个线程加入到一个group中,group会监控线程中的任务是否执行完毕,执行完毕可以发送通知,之后我们就可以做某项处理。示例如下:

        dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
        dispatch_queue_t serialQueue2 = dispatch_queue_create("cn.test.serial.queue2", NULL);
        dispatch_queue_t serialQueue3 = dispatch_queue_create("cn.test.serial.queue3", NULL);
        
        dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, serialQueue1, ^ {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"test");
        });
        dispatch_group_async(group, serialQueue2, ^ {
            [NSThread sleepForTimeInterval:3];
            NSLog(@"test2");
        });
        dispatch_group_async(group, serialQueue3, ^ {
            [NSThread sleepForTimeInterval:1];
            NSLog(@"test3");
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^ {
            NSLog(@"finished");
        });
    
    //代码执行结果
    test3
    test
    test2
    finished
    

    dispatch_group_notify函数会在group中所有线程中的任务执行完毕后被调用,因此上例不论前面线程执行谁先谁后,最后执行的一定是主线程中的block。例如有时候我们会通过多线程的方式去下载一些资源,数据等,然后所有下载完毕后在主线程中进行UI更新,就可以这样用。
    除了使用dispatch_group_notify来等待group中所有任务执行完毕外,还可以通过dispatch_group_wait函数来实现同样效果

        long ret = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        if (ret == 0) {
            NSLog(@"finished");
        }else
        {
            NSLog(@"unfinished");
        }
    
    

    需要说明的是该函数第二个参数是指等待时间,wait函数会使当前线程停止,等待直到:

    1. 在等待时间内,group中所有任务执行完毕,返回0
    2. 在等待时间内,group中任务没有执行完毕,超时返回非0

    如上例所示,当设置时间为DISPATCH_TIME_FOREVER意味着永久等待,直到任务执行完毕。

    dispatch_barrier_async

    当我们需要多个线程对某个属性进行频繁的读写操作时,如果不对线程加以控制,很容易造成读写混乱,因此我们可能希望:当进行读操作时,可以多个线程同时读取以保证高效率,而进行写操作时,不可以有任何线程进行读操作,写入完毕,线程又可以并发读取属性值,这种情况barrier技术正好可以方便的解决问题,示例如下

        dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(concurrentQueue, ^ { /*读取*/ });
        dispatch_async(concurrentQueue, ^ { /*读取*/ });
        dispatch_async(concurrentQueue, ^ { /*读取*/ });
        dispatch_barrier_async(concurrentQueue, ^ { /*写入*/});
        dispatch_async(concurrentQueue, ^ { /*读取*/ });
        dispatch_async(concurrentQueue, ^ { /*读取*/ });
    

    dispatch_sync

    有dispatch_async,当然也就有dispatch_sync,该函数表示同步执行,即当前线程会等待sync中的任务执行完毕才继续往下执行,例如

        dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
        dispatch_sync(serialQueue1, ^ {
            NSLog(@"test");
        });
    

    在主线程中执行上述代码,当执行到dispatch_sync时,线程会进行等待,直到追加到serialQueue1中的block执行完毕,主线程才继续往下执行。这种同步调用如果不加注意,就容易造成线程死锁问题,例如在主线程中执行下述代码就会造成死锁问题,主线程停止,等待lock执行完毕,而追加到主线程的block因为主线程停止而永远不会被执行,于是产生死锁。

        dispatch_sync(dispatch_get_main_queue(), ^ {
            NSLog(@"test");
        });
    

    dispatch_apply

    该函数与dispatch_sync有些关联,是指重复向某个队列追加block并等待全部block执行完毕后返回。例如当我们想遍历某个数组,对所有元素做些操作,可能会使用循环的方式,当数组非常大的时候,我们可能希望使用多线程去遍历数组所有元素来提高效率,那么就可以使用dispatch_apply,示例如下

        NSArray *array = /*数组赋值*/;
        dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_apply([array count], concurrentQueue, ^(size_t index) {
            NSLog(@"array item: %@", [array objectAtIndex:index]);
        });
    

    dispatch_apply的第一个参数指重复追加的次数,第二个为执行的队列,第三个为要执行的block,和前面不同的是,block里有一个参数,这个参数用来标识追加到队列中的每个block。上述代码中所有block在执行完毕后才能执行后续代码。

    dispatch_suspend/dispatch_resume

    这一对函数非常简单,dispatch_suspend用于挂起队列,之后队列中所有任务都会暂停执行,dispatch_resume使队列从暂停状态恢复为继续执行状态。

    dispatch semaphore

    dispatch_semaphore可以实现更加精细化地对线程进行管理。semaphore是拥有计数的信号量,我们可以通过semaphore对多线程访问共享资源时进行精细化的排他控制,也可以通过semaphore实现并发线程数量的控制。
    为了便于理解,我们举一个停车的例子,假设停车场只剩下两个车位,这时候同时来了三辆车,那么势必只能开入两辆车,另外一辆需要先一旁等待,直到有车离开停车位,等待的车才能开入,我们用信号量来实现这段逻辑:

        dispatch_semaphore_t sema = dispatch_semaphore_create(2);
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_async(globalQueue, ^ {
            dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
            NSLog(@"into 1");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"out 1");
            dispatch_semaphore_signal(sema);
        });
        dispatch_async(globalQueue, ^ {
            dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
            NSLog(@"into 2");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"out 2");
            dispatch_semaphore_signal(sema);
        });
        dispatch_async(globalQueue, ^ {
            dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
            NSLog(@"into 3");
            [NSThread sleepForTimeInterval:1];
            NSLog(@"out 3");
            dispatch_semaphore_signal(sema);
        });
    
    //代码执行结果
    2017-06-05 12:32:57.337284+0800 Demo[16270:3721871] into 1
    2017-06-05 12:32:57.337416+0800 Demo[16270:3721882] into 2
    2017-06-05 12:32:58.341023+0800 Demo[16270:3721882] out 2
    2017-06-05 12:32:58.341199+0800 Demo[16270:3721871] out 1
    2017-06-05 12:32:58.341402+0800 Demo[16270:3721870] into 3
    2017-06-05 12:32:59.345365+0800 Demo[16270:3721870] out 3
    

    操作信号量的主要有三个函数:
    dispatch_semaphore_create用来创建信号量,参数表示初始的信号总量。
    dispatch_semaphore_wait表示等待信号量,每执行一次信号量减1,第二个参数是指等待时间,DISPATCH_TIME_FOREVER意味着永久等待,该函数会使当前线程处于等待状态,直到以下两种情况才会返回:

    1. 在等待时间内,信号量大于等于1,这时wait函数返回0,信号量减1。
    2. 超出了等待时间,信号量依旧不满足大于等于1,wait函数因超时返回非0。

    dispatch_semaphore_signal表示发送信号量,每执行一次信号量加1
    我们也可以使用信号量对并发线程数量进行控制,示例如下:

        dispatch_semaphore_t sema = dispatch_semaphore_create(10);
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        for (int i = 0; i < 100; i++) {
            dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
            dispatch_async(globalQueue, ^ {
                NSLog(@"test: %d", i);
                [NSThread sleepForTimeInterval:1];
                dispatch_semaphore_signal(sema);
            });
        }
    
    

    该例可以这样理解,信号量初始为10,for循环在创建了10个线程后,信号量减为0,于是for循环就进行等待,直到某一个线程结束,增加了一个新的信号量,才能继续执行,这样便控制了并发数量不超过10个。

    dispatch_once

    该函数比较简单,常常用于单例的生成,它表示其代码块在程序中只会执行一次,用法如下:

        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"some init...");
        });
    

    虽然使用@synchronized也能保证单例的线程安全,但dispatch_once性能要远高于@synchronized,因此单例创建推荐使用dispatch_once

    dispatch I/O

    dispatch I/O是指使用多线程进行文件读取的技术。主要思想是当文件比较大时,我们可以将文件分段,然后使用多线程进行读取,再合并,这种方式可以大大提高文件读取速度。

    dispatch source

    dispatch source是指在内核发生各种事件时,开发者可以执行自定义处理的技术。内核中事件的类型有多种,最常见的一种是定时器事件,使用示例如下:

        dispatch_queue_t serialQueue1 = dispatch_queue_create("cn.test.serial.queue1", NULL);
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, serialQueue1);
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"test");
        });
        dispatch_resume(timer);
    

    上例创建了一个定时器,每两秒执行一次block块。dispatch_source_set_event_handler函数设置了每次定时器事件触发时执行的处理,通过dispatch_resume启动该定时器。
    当需要取消该定时器时,可以通过dispatch_source_cancel取消,另外还可以指定取消时要执行的处理。

        dispatch_source_cancel(timer);
        dispatch_source_set_cancel_handler(timer, ^ {
            NSLog(@"cancel");
        });
    

    最后

    GCD是基于内核级别的实现,在性能上是非常优异的,而且它语法简洁,是一套非常好的多线程编程方案。

    相关文章

      网友评论

        本文标题:Objective-C高级编程之GCD篇

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