iOS | Grand Central Dispatch (GCD)

进程和线程:

进程是一个正在运行的应用程序,一个应用程序可以对应一个或多个进程。应用程序是一个没有生命的实体,只有运行之后,才能成为一个活动的实体,也就是进程。

进程是操作系统分配资源的基本单元。进程在运行的过程中拥有独立的内存单元,一个进程崩溃后,不会对其他进程造成影响。

线程是独立运行和独立调度的基本单位,线程才是程序真正的执行单元,负责代码的执行。一个进程可以有一个或多个线程,同一个进程的线程共享进程的内存资源。线程没有单独的地址空间,一个线程崩溃整个进程就会崩溃。

多线程:

事实上,同一时间内单核CPU只能执行一个线程,多线程是CPU快速的在多个线程之间进行切换(调度),造成了多个线程同时执行的假象。如果是多核CPU就可以同时处理多个线程了。多线程是为了同步完成多项任务,提高系统的资源利用率来提高系统效率。

但是多线程也会造成各种问题,比如多个线程更新相同的资源会导致数据不一致(数据竞争),停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量内存等。

多线程优缺点:

多线程可以提高系统资源利用率,从而提高系统的效率。

缺点是开启多线程要花费时间和空间,开启过多的线程反而会降低性能,cpu频繁地在多个线程中调度会消耗大量的CPU资源,把CPU累死。

多线程使用场景:

实际开发中将一些耗时的操作放在子线程中执行,ios中默认有一个主线程,用来响应用户的手势和刷新UI,如果在主线程执行耗时操作会把页面卡死,直到执行完了这个操作才能操作界面。一定要在主线程刷新UI的原因:iOS为了保证效率,多线程是线程不安全的,在子线程刷新UI可能导致未知错误。

iOS中多线程的方案,各自特点:

Pthread是用c语言实现的,底层的多线程实现方案,需要程序员手动管理线程的生命周期(手动创建和销毁)。

NSThread 面向对象,需要程序员手动创建线程,但不需要手动销毁。子线程间通信很难。

Cocoa框架提供了NSObject类的performSelectorInBackground:withObject 实例方法和 performSelectorOnMainThread 实例方法。

- (void) launchThreadByNSObject_performSelectorInBackground_withObject
{
    [self performSelectorInBackground:@selector(doWork) withObject:nil];
}

- (void)doWork
{
    // ....
    // 做一些耗时操作,比如网络请求,数据库访问等。
    [self performSelectorOnMainThread:@selector(doneWork) withObject:nil waitUntilDone:NO];
    
}

-(void)doneWork
{
   // 刷新主界面
}

GCD是异步执行任务的技术之一。一般将程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。由于线程管理是系统的一部分来实现的,因此可同意管理,也可执行任务,这样就比以前的线程更有效率。

使用GCD实现上述功能更为简洁。充分利用了设备的多核,自动管理线程生命周期,更为高效。

dispatch_async(queue, ^{
    /*
    长时间处理,比如网络请求,数据库访问等。
     */
    dispatch_async(dispatch_get_main_queue(), ^{
        /*界面刷新等只在主线程执行的处理*/
    });
});

GCD的API

Dispatch Queue
dispatch_async(queue, ^{
   /*
   * 想要执行的任务
   */
});

Dispatch Queue 就是执行处理的等待队列,程序员在Block语法内记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue 按照追加的顺序(先进先出FIFO) 执行处理。

另外在执行处理时存在两种Dispatch Queue,一种是等待现在执行中处理的 Serial Dispatch Queue,它只使用一个线程;另一种是不等待现在执行中处理的 Concurrent Dispatch Queue,它使用多个线程并行执行多个追加处理。

以下是两种队列的测试结果:

  • Serial
dispatch_queue_t queue = dispatch_queue_create("hahaha", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    dispatch_async(queue, ^{
        NSLog(@"4");
    });
    dispatch_async(queue, ^{
        NSLog(@"5");
    });
    dispatch_async(queue, ^{
        NSLog(@"6");
    });
    dispatch_async(queue, ^{
        NSLog(@"7");
    });
2019-06-03 20:31:53.990446+0800 Test[6938:245001] 1
2019-06-03 20:31:53.990629+0800 Test[6938:245001] 2
2019-06-03 20:31:53.990718+0800 Test[6938:245001] 3
2019-06-03 20:31:53.990780+0800 Test[6938:245001] 4
2019-06-03 20:31:53.990856+0800 Test[6938:245001] 5
2019-06-03 20:31:53.990936+0800 Test[6938:245001] 6
2019-06-03 20:31:53.991013+0800 Test[6938:245001] 7
  • Concurrent
dispatch_queue_t queue = dispatch_queue_create("hahaha", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    dispatch_async(queue, ^{
        NSLog(@"4");
    });
    dispatch_async(queue, ^{
        NSLog(@"5");
    });
    dispatch_async(queue, ^{
        NSLog(@"6");
    });
    dispatch_async(queue, ^{
        NSLog(@"7");
    });
2019-06-03 20:30:34.914632+0800 Test[6904:243733] 3
2019-06-03 20:30:34.914632+0800 Test[6904:243732] 1
2019-06-03 20:30:34.914633+0800 Test[6904:243702] 2
2019-06-03 20:30:34.914649+0800 Test[6904:243734] 4
2019-06-03 20:30:34.914686+0800 Test[6904:243738] 5
2019-06-03 20:30:34.914722+0800 Test[6904:243739] 6
系统标准提供的Dispatch Queue

分别是Main Dispatch QueueGlobal Dispatch Queue

Main Dispatch Queue 是在主线程中执行的Dispatch Queue,主线程只有一个,所以它自然也是Serial Dispatch Queue。

另一个是Global Dispatch Queue,它是所有应用程序都能使用的Concurrent Dispatch Queue。

Global Dispatch Queue有四个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。

dispatch_set_target_queue
dispatch_queue_t queue = dispatch_queue_create("Test", NULL);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_set_target_queue(queue, globalQueue);

dispatch_set_target_queue 可以变更处理的优先级。不仅如此,还可以作成Dispatch Queue 的执行阶层。如果在多个 Serial Dispatch Queue 中用dispatch_set_target_queue 函数指定目标为某一个 Serial Dispatch Queue,那么原本应并行执行的多个Serial Dispatch Queue,在目标Serial Dispatch Queue上只能同时执行一个处理。

在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用dispatch_set_target_queue 函数将目标指定为某一个 Serial Dispatch Queue,即可防止处理并行执行。

dispatch_after

想在几秒后执行操作,可通过dispatch_after 函数来实现。

在3秒后将指定的 Block 追加到 Main Dispatch Queue中源码如下:

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);

dispatch_after(time, dispatch_get_main_queue(), ^{
    NSLog(@"Wait at least three seconds.");
});

PS: dispatch_after函数并不是在指定的时间后执行处理,而是在指定时间追加处理到Dispatch Queue。

Dispatch Group

在追加到 Dispatch Queue 中的多个处理全部结束后想执行结束处理,这种情况会经常出现。

只使用一个Serial Dispatch Queue 时,只要将想执行的处理全部追加到该Serial Dispatch Queue 中并在最后追加结束处理,即可实现。但是在使用Concurrent Dispatch Queue时或同时使用多个Dispatch Queue时,源代码就会变得复杂。

这是可以使用Dispatch Group。如下所示:追加3个Block到Global Dispatch Queue,这些Block如果全部执行完毕,就会执行Main Dispatch Queue 中结束处理用的Block。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
    NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"2");
});
dispatch_group_async(group, queue, ^{
    NSLog(@"3");
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    self.Label.text = @"完成";
    NSLog(@"hahaha");
});

运行结果:

2019-06-03 21:28:10.728780+0800 Test[7538:275043] 3
2019-06-03 21:28:10.728780+0800 Test[7538:275044] 1
2019-06-03 21:28:10.728780+0800 Test[7538:275041] 2
2019-06-03 21:28:10.738775+0800 Test[7538:274962] hahaha
dispatch_barrier_async

访问数据库或文件时,使用Serial Dispatch Queue 可避免数据竞争问题。

写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。

也就是说,为了高效率地进行访问,读取处理追加到 Concurrent Dispatch Queue 中,写入处理在任一个读取处理没有执行的状态下,追加到 Serial Dispatch Queue 中即可 (在写入处理结束之前,读取处理不可执行)。

利用Dispatch Group 和 dispatch_set_target_queue 函数可实现,但是源代码会很复杂。

我们可使用 dispatch_barrier_async 函数同 dispatch_queue_create 函数生成的 Concurrent Dispatch Queue 一起使用。

比如说如下代码可等待123read并行执行完之后,再执行write操作,然后此时只执行唯一的write操作,等write操作执行完后,再执行4567read操作。

dispatch_queue_t queue = dispatch_queue_create("Test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"1read %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"2read %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"3read %@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
    NSLog(@"write %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
   NSLog(@"4read %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"5read %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"6read %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"7read %@", [NSThread currentThread]);
});

执行结果:

2019-06-03 21:54:05.186307+0800 Test[7809:288931] 1read <NSThread: 0x600002378180>
2019-06-03 21:54:05.186309+0800 Test[7809:288934] 2read <NSThread: 0x600002378140>
2019-06-03 21:54:05.186320+0800 Test[7809:288935] 3read <NSThread: 0x600002374900>
2019-06-03 21:54:05.186454+0800 Test[7809:288935] write <NSThread: 0x600002374900>
2019-06-03 21:54:05.186538+0800 Test[7809:288935] 4read <NSThread: 0x600002374900>
2019-06-03 21:54:05.186551+0800 Test[7809:288934] 5read <NSThread: 0x600002378140>
2019-06-03 21:54:05.186553+0800 Test[7809:288931] 6read <NSThread: 0x600002378180>
2019-06-03 21:54:05.186569+0800 Test[7809:288933] 7read <NSThread: 0x600002374940>
dispatch_sync

意味着同步,也就是将指定的Block同步追加到 Dispatch Queue 中。在追加的 Block 结束之前,dispatch_sync 函数会一直等待。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{
    /*
    * 执行处理
    */
});

一旦调用 dispatch_sync 函数,那么在指定的处理执行结束之前,该函数不会返回。dispatch_sync 函数可简化为源代码,也可以说是简化版的dispatch_group_wait函数。

但 dispatch_sync 函数也很容易引起死锁。

dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    NSLog(@"hahaha");
});

比如在主线程中执行指定的Block并等待其执行结束。而其实在主线程中正在执行这些源代码,所以无法追加到主线程中同步执行Block。也就是说假设主线程是TaskA,加入的线程是TaskB,因为是同步的,所以TaskA没有执行完的话,TaskB不会执行,而TaskA执行完必然意味着TaskB也要执行完成。所以发生了死锁。

同样在Serial Dispatch Queue 中也会引起相同的问题

dispatch_queue_t queue = dispatch_queue_create("hahaha", NULL);
dispatch_async(queue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"haha");
    });
});
dispatch_apply

dispatch_apply 是 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按指定的次数将指定的Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, queue, ^(size_t index) {
    NSLog(@"%zu", index);
});
NSLog(@"done");

结果如下:

2019-06-03 23:31:20.492494+0800 Test[8500:325942] 0
2019-06-03 23:31:20.492496+0800 Test[8500:326017] 2
2019-06-03 23:31:20.492494+0800 Test[8500:326018] 1
2019-06-03 23:31:20.492495+0800 Test[8500:326019] 4
2019-06-03 23:31:20.492499+0800 Test[8500:326023] 3
2019-06-03 23:31:20.492637+0800 Test[8500:325942] done

因为在Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是输出结果中的done必定在最后的位置上。这是因为dispatch_apply 函数会等待全部处理执行结束。

dispatch_suspend / dispatch_resume

当追加大量处理到 Dispatch Queue 时,在追加处理的过程中,有时希望不执行已追加的处理。例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。

这种情况下,只要挂起指定的 Dispatch Queue。当可以执行时再恢复。

dispatch_suspend 挂起和 dispatch_resume 恢复。

dispatch_queue_t queue = dispatch_queue_create("com.test.gcd", DISPATCH_QUEUE_SERIAL);
//提交第一个block,延时5秒打印。
dispatch_async(queue, ^{
    sleep(5);
    NSLog(@"After 5 seconds...");
});
//提交第二个block,也是延时5秒打印
dispatch_async(queue, ^{
    sleep(5);
    NSLog(@"After 5 seconds again...");
});
//延时一秒
NSLog(@"sleep 1 second...");
sleep(1);
//挂起队列
NSLog(@"suspend...");
dispatch_suspend(queue);
//延时10秒
NSLog(@"sleep 10 second...");
sleep(10);
//恢复队列
NSLog(@"resume...");
dispatch_resume(queue);

打印结果:

2019-06-03 23:44:01.254454+0800 Test[8617:332248] sleep 1 second...
2019-06-03 23:44:02.255484+0800 Test[8617:332248] suspend...
2019-06-03 23:44:02.255662+0800 Test[8617:332248] sleep 10 second...
2019-06-03 23:44:06.257416+0800 Test[8617:332298] After 5 seconds...
2019-06-03 23:44:12.256036+0800 Test[8617:332248] resume...
2019-06-03 23:44:17.257411+0800 Test[8617:332297] After 5 seconds again...

可以看出dispatch_suspend并不会立即暂停正在运行的block,而是在当前block执行完成后,暂停后续的block执行。

Dispatch Semaphore
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 100; i++)
{
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 执行到此处semaphore的值恒为0
        [array addObject: [NSNumber numberWithInt:i]];
    	// 排他控制结束
        // 通过dispatch_semaphore_signal 函数将semaphore计数值加1
        // 由最先通过wait函数等待semaphore计数值加一的线程继续执行。
        dispatch_semaphore_signal(semaphore);
    });
}
NSLog(@"完成");

·dispatch_semaphore_wait函数会一直等待直到semaphore信号量值变为1,由于semaphore的计数值大于等于1,所以可以将其计数值减去1,wait函数执行返回。所以才有上面注释的恒为0的意思。

dispatch_once

保证在应用程序中只执行一次指定处理的API。

static dispatch_once_t pred;
dispatch_once (&pred, ^{
   /* 
   * 初始化
   */
});

这就是我们常说的单例模式。

Dispatch I/O

读取大文件时,如果将文件分成合适大小并使用 Global Dispatch Queue 并列读取的话,会增加读取速度。

dispatch_async(queeu, ^{/* 读取 0 ~ 8191 字节 */});
dispatch_async(queeu, ^{/* 读取 8192 ~ 16383 字节 */});
dispatch_async(queeu, ^{/* 读取 16384 ~ 24575 字节 */});
dispatch_async(queeu, ^{/* 读取 24576 ~ 32767 字节 */});
dispatch_async(queeu, ^{/* 读取 32769 ~ 40959 字节 */});
dispatch_async(queeu, ^{/* 读取 40960 ~ 49151 字节 */});
dispatch_async(queeu, ^{/* 读取 49152 ~ 57343 字节 */});
dispatch_async(queeu, ^{/* 读取 57344 ~ 65535 字节 */});

可像上面这样,将文件分割为一块一块地进行读取处理。分割读取的数据通过使用Dispatch Data 可更为简单地进行结合和分割。

代码解读可参考:https://www.jianshu.com/p/da4710d76d33

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值