iOS多线程:深入理解GCD

iOS与OS X多线程和内存管理总结

Posted by lingjye on August 16, 2018

什么是GCD?

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

示例:

1
2
3
4
5
6
7
8
9
dispatch_async(queue, ^{
	// 执行耗时操作
	// 网络请求,数据库访问等
	
	// 处理完毕,返回主线程
	dispatch_async(dispatch_get_main_queue(), ^{
		// 刷新UI等操作
	})
})

上述示例也可以用Cocoa框架中NSObject类的performSelectorInBackground:withObject:和performSelectorOnMainThread:withObject:waitUntilDone:方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)dispatch {
    // 调用后台线程方法
    [self performSelectorInBackground:@selector(doSomeWork) withObject:nil];
}

- (void)doSomeWork {
    // 后台线程处理方法
    // 执行耗时操作
    
    // 执行耗时操作完成,返回主线程处理
    [self performSelectorOnMainThread:@selector(doneWork) withObject:nil waitUntilDone:NO];
}

- (void)doneWork {
    // 需要主线程可以处理的操作
    // 例如刷新UI等
}

当然也可以使用NSThread类来完成上述操作,但相比之下依然可以看出使用GCD进行多线程编程更为简洁。另外还可以通过GCD提供的系统级线程管理提高执行效率。

什么是多线程编程

一般来说线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。好比一个CPU命令自上而下顺序执行一条命令即为一个线程,并且一个CPU核一次也只能执行一个命令。

当然,一个CPU核也可以执行多个命令,也称之为多线程,但是此时CPU执行命令时实质上是在频繁切换执行的任务而已。此时CPU的寄存器等信息会被保存到各个命令专用内存中,每次执行其他命令时从切换目标任务专用的内存块中复原寄存器等信息,然后继续执行切换目标任务的CPU命令,这一过程称为“上下文切换”(这也是需要耗费性能的,iOS协程可以参考淘宝的coobjc)。所以在单核CPU中使用多线程实质上就是多个线程之间多次进行上下文切换,而在多核CPU中(线程数小于CPU核数)的多线程才是真的并行执行多线程技术。

这种利用多线程编程的技术被称为“多线程编程”。

存在问题

在使用多线程编程的同时,需要注意的的是多线程带来的不仅是便利,还有可能发生的问题。比如多个线程更新统一资源时导致数据不一致的数据竞争问题,死锁,线程创建过多消耗大量内存等,还有就是难以定位,在非主线程操作UI等。

好处

使用多线程编程最大的好处就是可以保证应用程序的性能,提升用户体验。一般程序卡顿的主要原因都是发生在主线程,所以我们一定要尽可能的去保证主线程不发生阻塞。例如程序启动时,我们通常将不一些耗时操作放到异步线程中操作,以保证主线程操作能够正常且流程的进行。而GCD则大大简化了偏于复杂的多线程编程的源代码。

Dispatch Queue

苹果官方所说:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。

1
2
3
dispatch_async(queue, ^{
	// 加入想执行的任务
})

我们通过dispatch_async方法,在block中添加想要执行的操作然后追加到Dispatch Queue中。其中Dispatch Queue按照FIFO(First-In-First-Out先进先出)原则执行处理。

GCD的API全部为包含在libdispatch库中的C语言函数,Dispatch Queue通过结构体和链表,被实现为FIFO队列。FIFO队列管理是通过dispatch_async等函数所追加的Block。Block并不是直接加入FIFO队列,而是先加入Dispatch Continuation这一dispatch_continuation_t类型的结构体中,然后再加入FIFO队列。该Dispatch Continuation用于记录Block所属的Dispatch Group和其他的信息(也称作执行上下文)。

Dispatch Queue分为两种:

Dispatch Queue种类 说明 使用线程情况
Serial Dispatch Queue 等待现在执行中处理结束 使用一个线程,串行
Concurren Dispatch Queue 不等待现在执行处理结束 使用多个线程,并行

iOS和OSX的内核(XNU内核)决定应当使用的线程数,并只生成所需的线程进行处理。在处理结束时,剩余所需执行的处理就会减少,此时XNU内核会结束不再需要的线程。并且Concurrent Dispatch Queue会自行管理多个处理中的线程,CPU核心数一定情况下新任务会被添加到闲置线程中去,但是执行的顺序会跟处理的任务本身和系统状态有关。

Dispatch Queue的创建方法有两种:

  1. dispatch_queue_create
  2. Main Diaptch Queue/Global Dispatch Queue

dispatch_queue_create

这是通过GCD的API生成的Dispatch Queue。

创建Dispatch Queue源码,解释请阅读注释:

1
2
3
4
5
6
7
8
9
10
11
/**
 创建Dispatch Queue
 @label "com.test.mySerialDispatchQueue" 第一个参数 指定Serial Diaptch Queue的名称, 推荐使用bundleID这种域名倒写。
 会在Xcode和Instruments调试器中作为Dispatch Queue的名称显示,并且也会出现在程序崩溃的CrashLog中(崩溃日期)。
 可以 NULL也可以,但不推荐
 
 @attr 如果生成Serial Dispatch Queue,将第二个参数指定为NULL, 或者DISPATCH_QUEUE_SERIAL(DISPATCH_QUEUE_SERIAL的宏定义也是NULL)
 如果生成Concurrent Dispatch Queue,将第二个参数指定为DISPATCH_QUEUE_DISPATCH_QUEUE_CONCURRENT
 */
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.test.mySerialDispatchQueue", NULL);
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.test.mySerialDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

需要指出的是,使用该方法生成的各个Serial Dispatch Queue间是并行执行的,并且没有数量限制,但是考虑到内存因素,还是适当创建较好。

另外,使用该API生成的Dispatch Queue在iOS 6。0之前需要手动调用dispatch_release(queue)释放,iOS 6.0之后被纳入ARC管理范畴。

Dispatch Queue没有“取消”这一概念,一旦处理加入到Dispatch Queue,是没有办法去除该处理的。此时可使用NSOperationQueue等其他方法。

Main Dispatch Queue/Global Dispatch Queue

该方法是获取系统标准提供的Dispatch Queue

Main Dispatch Queue指的是主线程,程序中只有1个,属于Serial Dispatch Queue。追加到它上边的任务在主线程的RunLoop中执行,执行UI等操作也一定要追加到Main Dispatch Queue中执行,在其他线程中更新UI虽然xcode不会报错,但是潜在风险,我们还是有必要规避的。Main Dispatch Queue在RunLoop中执行Block。

Global Dispatch Queue是所有应用程序都可以使用的Concurrent Dispatch Queue,所以使用多线程过程中一般不用使用dispatch_queue_create函数,它有四个优先级。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取主线程Main Dispatch Queue方法
    dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
    
// 获取Global Dispatch Queue(高优先级)方法
dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
// 获取Global Dispatch Queue(默认优先级)方法
dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
// 获取Global Dispatch Queue(低优先级)方法
dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    
// 获取Global Dispatch Queue(后台优先级)方法
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

注意:通过XNU内核用于Global Dispatch Queue的线程并不能保证实时性,所以优先级只是大致判断,并不保证一定按这种规则执行。

使用方法:

1
2
3
4
5
6
7
8
9
// 在默认优先级的Global Dispatch Queue中执行Block
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行耗时操作或可并行执行的处理
    
    // 在Main Dispatch Queue中执行Block
    dispatch_async(dispatch_get_main_queue(), ^{
        // 只能在主线程中进行的操作
    });
});

dispatch_set_target_queue

由于dispatch_queue_create函数生成的Dispatch Queue都使用与默认优先级Global Dispatch Queue相同优先级的线程。所以使用dispatch_set_target_queue变更执行优先级。

1
2
3
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.test.mySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

注意该函数的第一个参数不要使用Main Dispatch Queue和Global Dispatch Queue。

该函数还可以改变Dispatch Queue的执行阶层,比如多个并行执行Serial Dispatch Queue可以指定到一个Serial Dispatch Queue上,使他们变成串行。

dispatch_after

该函数主要进行是延时执行操作。

1
2
3
4
5
6
7
 /**
 * dispatch_time 计算相对时间 dispatch_walltimey函数用于计算绝对时间
 * NSEC_PER_SEC是秒为单位,以毫秒为单位使用NSEC_PER_MSEC
 */
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 等待至少3s后执行的操作
});

注意该方法并不是在指定时间后执行,而是跟追加到的Dispatch Queue有关,例如以上代码中的主线程中有大量处理追加任务或者主线程本身有延迟则实际上等待时间会更长,至少会是3s。

Dispatch Group

使用多个Concurrent Dispatch Queue或者多个Dispatch Queue时,最后执行同步操作源代码会比较复杂,此时可使用Dispatch Group。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建Dispatch Group
dispatch_group_t group = dispatch_group_create();
// 获取一个默认优先级的Global Dispatch Queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 使用dispatch_group_async方法将Queue指定到Group中管理
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加入监视,等待group中的所有任务处理结束时调用该方法
// 所有block执行完成,调用该方法打印“4”
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"4");
});

除了dispatch_group_notify还可以使用dispatch_group_wait函数,此函数仅等待所有处理执行完毕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第二个参数可指定dispatch_time_t类型的值,代表等待时间
// DISPATCH_TIME_FOREVER代表永远等待
// DISPATCH_TIME_NOW代表不用一直等待
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
// 如果没有使用DISPATCH_TIME_FOREVER,可使用dispatch_group_wait的返回值判断任务是否全部执行完成
// ull代表是unsigned long long类型
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
    // Dispatch Group的所有任务处理完成
} else {
    // Dispatch 的某些处理还在执行,此处result>=1
}

还可以在dispatch_async(queue, block)中使用dispatch_group_enter()和dispatch_group_leave配合dispatch_group_notify来完成一些操作。比如图片上传,文件读取等。

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < imgs.count; i++) {
	dispatch_group_enter(group)
	// 上传操作
	// do upload
	
	// 上传完成调用
	dispatch_group_leave(group)
}

dispatch_group_notify(group, queue, ^{
	// 所有任务执行完成
});

使用dispatch_group_notify函数追加处理到Main Dispatch Queue中可以简化代码。

dispatch_barrier_async

dispatch_barrier_async一般也称作栅栏函数。该函数同dispatch——queue_create函数生成的Concurrent Dispatch Queue一起使用,可实现高效的数据库访问和文件访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 创建Concurrent Dispatch Queue, 执行多个并行读取操作
dispatch_queue_t queue = dispatch_queue_create("com.test.DispatchBarrierAsync", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
   // read1
    NSLog(@"read1:%@", self.title);
});
dispatch_async(queue, ^{
    // read2
    NSLog(@"read2:%@", self.title);
});
dispatch_async(queue, ^{
    // read3
    NSLog(@"read3:%@", self.title);
});
// dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue上的并行执行的处理全部结束后执行,然后dispatch_barrier_async追加的处理执行完毕,继续执行后边的Concurrent Dispatch Queue处理。
dispatch_barrier_async(queue, ^{
   // write 保证后面的异步执行访问不错乱
    self.title = @"dispatch_barrier_async";
    NSLog(@"write:%@", self.title);
});
dispatch_async(queue, ^{
    // read4
    NSLog(@"read4:%@", self.title);
});
dispatch_async(queue, ^{
    // read5
    NSLog(@"read5:%@", self.title);
});

dispatch_sync / dispatch_async

dispatch_sync表示同步,在追加Block结束之前,该函数会一直等待。dispatch_async表示异步,不会等待追加的Block执行完毕。

使用dispatch_sync容易造成死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 死锁1 当前执行的任务处于主线程中,该函数会一直等待主线程结束,造成死锁
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    NSLog(@"死锁");
});
    
// 死锁2
// dispatch_async不会一直等待
dispatch_async(queue, ^{
    // 此处死锁同死锁1
    dispatch_sync(queue, ^{
        NSLog(@"死锁");
    });
});
    
// 死锁3
// 使用Serial Dispatch Queue引起死锁
dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.test.serialDispatchQueue", NULL);
dispatch_async(serialDispatchQueue, ^{
    dispatch_sync(queue, ^{
        NSLog(@"死锁");
    });
});

另外还有dispatch_barrier_async/dispatch_barrier_sync函数,使用过程中也需要注意死锁问题。

dispatch_apply

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

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 第一个参数代表重复执行次数, 第二个参数为追加对象的Dispatch Queue,第三个参数为追加的处理,并将当前执行任务执行下标作为参数传入Block
dispatch_apply(10, queue, ^(size_t index) {
    // 执行10次,并发执行
    NSLog(@"%zu", index);
});
// 该打印方法一定最后执行, 因为dispatch_apply函数会等待全部处理执行完成
NSLog(@"结束");

由于dispatch_apply函数会等待所有的处理执行结束,因此推荐在dispatch_async函数中非同步地执行dispatch_apply函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 配合dispatch_async使用
dispatch_async(queue, ^{
    dispatch_apply(10, queue, ^(size_t index) {
        // 处理操作
        NSLog(@"%zu", index);
    });
    
    // dispatch_apply函数处理完毕后,在Main Dispatch Queue中非同步执行
    dispatch_async(dispatch_get_main_queue(), ^{
        // 主线程操作
        NSLog(@"结束");
    });
});

dispatch_suspend / dispatch_resume

dispatch_suspend函数用来挂起指定的Dispatch Queue,挂起后追加到Dispatch Queue中尚未执行的操作将在此之后停止执行,直到使用dispatch_resume来恢复这些操作。需要注意的是,已经(正在)执行的操作不会停止。

dispatch_resume函数用来恢复指定的Dispatch Queue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dispatch_queue_t queue = dispatch_queue_create("com.test.dispatchSuspendResume", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    NSLog(@"等待3s前");
    sleep(3);
    // 已开始任务不会被挂起
    NSLog(@"等待3s后");
});
dispatch_async(queue, ^{
    NSLog(@"另一个等待3s前");
    sleep(3);
    // 这个打印方法会在上次打印后至少间隔6s后打印
    // 本次未开始,被挂起
    NSLog(@"另一个等待3s后");
});
//延时一秒
sleep(1);
//挂起队列
NSLog(@"开始挂起");
dispatch_suspend(queue);
//延时5秒
NSLog(@"等待5s");
sleep(5);
//5s后恢复队列
NSLog(@"恢复");
dispatch_resume(queue);

Dispatch Semaphore

我们所说的信号量指的就是Dispatch Semaphore,可用来控制访问资源的数量的标识。

它有三个函数分别是:

1
2
3
4
5
6
// 创建信号量,并设置信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(10)
// 发送一个信号
dispatch_semaphore_signal(signal)
//等待信号量,可以使信号量减1,当信号总量为0时一直等待(阻塞当前所在线程),知道信号总量大于0
dispatch_semaphore_wait(signal,DISPATCH_TIME_FOREVER)

Dispatch Semaphore主要可用于:保证线程安全(类似于加锁),将异步转换为同步。

使用场景示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用Global Dispatch Queue更新NSNutableArary类对象,容易造成内存错误的异常,可使用Dispatch Semaphore
NSMutableArray *mutArray = [NSMutableArray array];
// dispatch semaphore 的计数初始值设定为1
// 保证可访问 NSMutableArray 类对象的线程同时只能有一个
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
for (int i = 0; i < 100; i++) {
    dispatch_async(queue, ^{
        // 等待 dispatch semaphore
        // 直到 dispatch semaphore 的计数值达到大于等于1,保证了访问NSMutableArray的线程只有一个
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        
        // 大于等于1 所以讲dispatch semaphore 的技术值减1
        //                        dispatch_semaphore_wait 函数执行返回
        [mutArray addObject:[NSNumber numberWithInt:i]];
        
        // 通过 dispatch_semaphore_signal 函数
        // 将 dispatch semaphore 的计数值增加1
        dispatch_semaphore_signal(semaphore);
    });
}

dispatch_once

dispatch_once函数保证应用程序只会执行一次指定的处理,并且可以保证线程安全。例如生成一个单例对象:

1
2
3
4
5
6
7
8
- (instancetype)dispatchOnce {
    static dispatch_once_t onceToken;
    static MysingleTon *singleTon;
    dispatch_once(&onceToken, ^{
        singleTon = [[NSObject alloc] init];
    });
    return singleTon;
}

Dispatch I/O

Dispatch I/O可以在读写文件时,使用Global Dispatch Queue将一个文件按某个大小分别进行读取,可提升读取速度。

1
2
3
4
5
6
7
8
9
10
11
12
// 获取Global Dispatch Queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 将文件分为三块读取
dispatch_sync(queue, ^{
    // 读取0~8191字节
});
dispatch_sync(queue, ^{
    // 读取8192~16383字节
});
dispatch_sync(queue, ^{
    // 读取16384~24575字节
});

分割读取数据使用Dispatch Data可更为简单的进行结合和分割。

异步串行读取 Serial Dispatch Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dispatch_queue_t queue = dispatch_queue_create("PipeQ", NULL);
dispatch_fd_t fd = open([path UTF8String], O_RDONLY, 0);
dispatch_io_t pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, queue, ^(int error) {
    close(fd);
});
//设置读取大小
size_t water = 1024;
dispatch_io_set_low_water(pipe_channel, water);
dispatch_io_set_high_water(pipe_channel, water);

long long fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil].fileSize;
NSMutableData *totalData = [[NSMutableData alloc] init];

dispatch_io_read(pipe_channel, 0, fileSize, queue, ^(bool done, dispatch_data_t _Nullable data, int error) {
    if (error == 0) {
        size_t len = dispatch_data_get_size(data);
        if (len > 0) {
            [totalData appendData:(NSData *)data];
        }
    }
    if (done) {
        NSString *str = [[NSString alloc] initWithData:totalData encoding:NSUTF8StringEncoding];
        NSLog(@"%@", str);
    }
});

异步并行读取文件 Concurrent Dispatch Queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
dispatch_queue_t queue = dispatch_queue_create("PipeQ", DISPATCH_QUEUE_CONCURRENT);
dispatch_fd_t fd = open([path UTF8String], O_RDONLY);
dispatch_io_t pipe_channel = dispatch_io_create(DISPATCH_IO_RANDOM, fd, queue, ^(int error) {
    close(fd);
});
off_t currentSize = 0;
long long fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil].fileSize;
//设置每次读取大小
size_t offset = 1024;
// 创建Dispatch Group
dispatch_group_t group = dispatch_group_create();
// 创建长度为文件长度的空可变data,等待数据读取到之后替换为读取到的数据
NSMutableData *totalData = [[NSMutableData alloc] initWithLength:fileSize];
for (; currentSize <= fileSize; currentSize += offset) {
    dispatch_group_enter(group);
    dispatch_io_read(pipe_channel, currentSize, offset, queue, ^(bool done, dispatch_data_t _Nullable data, int error) {
        if (error == 0) {
            size_t len = dispatch_data_get_size(data);
            if (len > 0) {
                const void *bytes = NULL;
                (void)dispatch_data_create_map(data, (const void **)&bytes, &len);
                // 替换数据
                [totalData replaceBytesInRange:NSMakeRange(currentSize, len) withBytes:bytes length:len];
            }
        }
        if (done) {
            dispatch_group_leave(group);
        }
    });
}
dispatch_group_notify(group, queue, ^{
    NSString *str = [[NSString alloc] initWithData:totalData encoding:NSUTF8StringEncoding];
    NSLog(@"%@", str);
});

Dispatch Source

Dispatch Sources是协调特定低级别系统事件处理的基本数据类型,它是BSD系内核惯有功能kqueue的包装。kqueue的CPU负荷非常小,是XUN内核中应用处理各种发生事件的技术。

GCD支持以下类型的Dispatch Sources:

  • Timer dispatch sources 生成定时通知。
  • Signal dispatch sources 当UNIX信号(分时处理器信号)到达时通知你
  • Descriptor sources 会根据多种基于文件信息或socket操作通知你,例如
    • 当数据可读时
    • 当数据可写时
  • 当文件在文件系统中被删除/移动/重命名时
  • 当文件元信息更改时
  • Process dispatch sources 通知进程相关事件,例如
    • 当一个进程存在时
    • 当一个进程发出fork或exec类型的调用时
    • 当一个信号传递到进程时
  • Mach port dispatch sources 通知Mach相关事件
  • Custom dispatch sources 是自定义自触发

Dispatch Source可以取消,并且取消时还可使用Block进行处理,相比kqueue更为简单,在必须使用kqueue时推荐使用Dispatch Source。

Dispatch Source的种类:

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

异步文件读取示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
NSString *path = [[NSBundle mainBundle] pathForResource:@"DispatchIOFile" ofType:@"txt"];
__block size_t total = 0;
// 要读取的字节数
long long fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil].fileSize;
size_t size = fileSize;
char *buff = (char *)malloc(size);
    
dispatch_fd_t fd = open([path UTF8String], O_RDONLY);
// 设定异步映像
fcntl(fd, F_SETFL, O_NONBLOCK);
    
// 获取用于追加事件的Global Dispatch Queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
// 基于READ事件创建Dispatch Source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, fd, 0, queue);
// 指定发生READd事件时执行的处理
dispatch_block_t event_handler = ^{
    // 获取可读取的字节数
    size_t availble = dispatch_source_get_data(source);
    // 从映像中读取
    long length = read(fd, buff, availble);
    // 发生错误时取消Dispatch Source
    if (length < 0) {
        // 取消读取
        dispatch_source_cancel(source);
    }
    total += length;
    if (total == size) {
        // buff的处理
        // 处理结束,取消Dispatch Source
        dispatch_source_cancel(source);
        NSString *result = [[NSString alloc] initWithBytes:buff length:size encoding:NSUTF8StringEncoding];
        NSLog(@"%@", result);
        NSLog(@"读取完成");
    }
};
dispatch_source_set_event_handler(source, event_handler);
    
// 指定取消Dispatch Source时的处理
dispatch_block_t cancel_handler = ^{
    // 释放资源
    free(buff);
    close(fd);
    // 释放Dispatch Source 支持ARC,无需手动释放
//        dispatch_release(source);
    NSLog(@"取消");
};
dispatch_source_set_cancel_handler(source, cancel_handler);
    
// 启动 Dispatch Source
dispatch_resume(source);

Dispatch Source定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 指定DISPATCH_SOURCE_TYPE_TIMER,创建Dispatch Source, 指定在Main Dispatch Queue
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 定时器每隔3s执行一次,允许延迟1s
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
// 指定执行处理
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"定时器执行");
    // 取消Dispatch Source
//        dispatch_source_cancel(timer);
});

// 指定取消事件
dispatch_source_set_cancel_handler(timer, ^{
    NSLog(@"取消");
});

dispatch_resume(timer);
// 必须设置为属性才有用
_timer = timer;

本文Demo