iOS-多线程(三)-GCD函数
单次函数dispatch_once
单次函数一般用来创建单例或者是执行只需要执行一次的程序。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"==只会执行一次的代码==");
});
void dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
dispatch_once
会保证block
中的程序只执行一次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
迭代函数dispatch_apply
dispatch_apply
函数会按照指定的次数将指任务添加到指定的队列中进行执行。无论是在串行队列,还是并发队列中,dispatch_apply
都会等待全部任务执行完毕。
如果是在串行队列中使用dispatch_apply
,会按顺序同步执行,就和普通的for
循环类似;如果是在异步队列中使用,下标可能不是按顺序来的。
void
dispatch_apply(size_t iterations,
dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue,
DISPATCH_NOESCAPE void (^block)(size_t));
-
iterations
:执行迭代的次数 -
queue
:执行迭代的队列,建议使用DISPATCH_APPLY_AUTO
,会自动调用合适的线程队列 -
void (^block)(size_t))
:迭代的结果回调
延迟函数dispatch_after
延迟函数的作用是在指定的队列中,按照给定的时间执行一个操作。
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t block);
-
dispatch_time_t when
:指定执行任务的时间。- 可以使用
DISPATCH_TIME_NOW
,但是不推荐,因为该函数调用了dispatch_async
- 也可以使用
dispatch_time
或者dispatch_walltime
自定义时间:dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC))
- 不能使用
DISPATCH_TIME_FOREVER
- 可以使用
-
dispatch_queue_t queue
:指定队列,执行任务的队列。 -
dispatch_block_t block
:要执行的任务,不能传NULL
。
调度组函数dispatch_group
通过Dispatch Group
,我们可以将多个任务放入一个组中,并且可以让他们在同一队列或不同队列上异步执行,执行完成之后,再执行其他的依赖于这些任务的操作。
相关API
:
- 创建调度组
dispatch_group_t dispatch_group_create(void);
- 进组,开始执行组内任务
void dispatch_group_enter(dispatch_group_t group);
- 出组,组任务执行完成
void dispatch_group_leave(dispatch_group_t group);
- 同步等待,阻塞当前线程直到组的任务都执行完成或者
timeout
归零才会继续下一步
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
- 组所关联的所有任务已经完成,发出一个通知告知
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
下面我们通过一个例子来看一下dispatch_group
的使用:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"==1==");
});
dispatch_async(queue, ^{
NSLog(@"==2==");
});
dispatch_async(queue, ^{
NSLog(@"==3==");
});
dispatch_group_notify(group, queue, ^{
NSLog(@"===4=");
});
运行程序,控制台输出:
image可以看出这并不是我们想要的结果。对程序进行修改,继续运行:
image同样使用dispatch_group_wait
也会得到相应的结果:
但是dispatch_group_wait
会阻塞之后的操作,比如我们在组通知之后还执行了NSLog(@"==5==")
,组任务并没有阻塞到它的执行,而dispatch_group_wait
就会阻塞。
注意,dispatch_group_enter
和dispatch_group_leave
必须成对出现,否则会造成死锁。
栅栏函数dispatch_barrier
栅栏函数分为dispatch_barrier_async
和dispatch_barrier_sync
函数,这两个函数既有共同点,又有不同点:
- 共同点:
- 等待在它前面插入队列的任务先执行完
- 等待他们自己的任务执行完再执行后面的任务
- 不同点:
-
dispatch_barrier_sync
将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们 -
dispatch_barrier_async
将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。
下面我们配合一个例子说明一下:
- (void)barrierAsync {
dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"--1--");
});
dispatch_async(queue, ^{
NSLog(@"--2--");
});
dispatch_barrier_async(queue, ^{
NSLog(@"--barrier_async--%@--",[NSThread currentThread]);
sleep(2);
});
NSLog(@"=======barrierAsync=======");
dispatch_async(queue, ^{
NSLog(@"--3--");
});
dispatch_async(queue, ^{
NSLog(@"--4--");
});
dispatch_async(queue, ^{
NSLog(@"--5--");
});
}
运行程序:
image将dispatch_barrier_async
函数改为dispatch_barrier_sync
,然后运行程序:
通过打印结果可以看出栅栏函数不管是同步异步,都会对当前队列中的任务起到隔离作用,就是会让栅栏之前的多线程操作先执行,让栅栏之后的多线程操作后执行。不同的是dispatch_barrier_async
函数之后的多线程操作都是并发执行,而dispatch_barrier_sync
之后的操作都是同步执行,所以我们打印的barrierAsync
的执行顺序和barrierSync
不同。
简而言之,dispatch_barrier_sync
和dispatch_barrier_async
都会隔离队列中栅栏前后的任务,不同的是会不会阻塞当前队列。所以栅栏函数和其拦截的任务必须是同一队列的,不然没有阻塞效果。所以在AFN
中使用栅栏函数没有效果,AFN
自己维护了一个串行队里,除非使用这个队列才会起作用。
注意,当我们在主线程中调用任务,而且将同步栅栏函数也添加到主队列中,会发生死锁现象。使用栅栏函数要使用自定义队列,防止阻塞、死锁。
信号量dispatch_semaphore_t
一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
相关API
:
- 创建信号量,参数:信号量的初值,如果小于0则会返回
NULL
,该参数控制当前能开启的线程数量。
dispatch_semaphore_t dispatch_semaphore_create(long value)
- 等待(减少)信号量,信号出现之后才会返回。
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
-
dispatch_semaphore_t dsema
: 信号量。如果传入的dsema
大于0,就继续向下执行,并将信号量减1;如果dsema
等于0,阻塞当前线程等待资源被dispatch_semaphore_signal
释放。如果等到了信号量,继续向下执行并将信号量减1,如果一直没有等到信号量,就等到timeout
再继续执行。 -
dispatch_time_t timeout
: 超时,阻塞线程的时长。一般传DISPATCH_TIME_FOREVER
或者DISPATCH_TIME_NOW
,也可以自定义。dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 1*100*100*100);
-
如果成功则返回0,超时会返回其他值
- 发信号(增加信号量)。如果之前的值小于零,该函数会唤醒等待的线程
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
减少和增加信号量通常成对使用,使用的顺序是先减少信号量(wait)
然后再增加信号量(signal)
。
下面我们结合一个例子,说明一下信号量的使用:
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务1");
sleep(1);
NSLog(@"任务1完成");
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务2");
sleep(1);
NSLog(@"任务2完成");
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"执行任务3");
sleep(1);
NSLog(@"任务3完成");
dispatch_semaphore_signal(semaphore);
});
运行程序,控制台输出:
image将创建的信号量改为2:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
image
将创建的信号量改为3,或者大于3:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
image
同理,我们还可以将例子中的并发任务改为同步任务。可以得出如下结论:
- 如果是同步任务,不管创建的信号量和任务数的关系,都是按照顺序一个接一个执行
- 如果是异步任务:
- 创建的信号量小于任务数,就会先按照信号量的数量执行相应的任务,剩下任务会等到之前执行的任务执行完成才会接着执行
- 创建的信号量大于等于任务数,所有任务都会并发执行
再来看一个例子:
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
NSLog(@"==a==%d==", a);
由于异步线程的问题,我们打印a
的值,可能是大于等于5,此时依靠信号量就可以控制让循环外输出a=5
。如下:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
NSLog(@"==a==%d==", a);
关于信号量的时候,我们需要注意的是防止线程被阻塞,当执行dispatch_semaphore_wait
方法的时候一定要保证传入的信号量大于0。
调度源函数dispatch_source
当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。也就是用GCD
的函数指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的队列即可,当监听到指定的系统事件发生时会调用回调函数,将该回调函数作为一个任务放入指定的队列中执行。
相关的API
:
- 创建源
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t _Nullable queue);
- 设置源事件回调
void
dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t _Nullable handler);
- 设置源事件数据
void
dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
- 获取源事件数据
unsigned long
dispatch_source_get_data(dispatch_source_t source);
获取的数据类型和源事件的类型相关:
- 读文件类型的
dispatch_source
,返回的是读到文件内容的字节数。
- 写文件类型的
dispatch_source
,返回的是文件是否可写的标识符,正数表示可写,负数表示不可写。 - 监听文件属性更改类型的
dispatch_source
,返回的是监听到的有更改的文件属性,用常量表示,比如DISPATCH_VNODE_RENAME
等。 - 进程类型的
dispatch_source
,返回监听到的进程状态,用常量表示,比如DISPATCH_PROC_EXIT
等。 -
Mach
端口类型的dispatch_source
,返回Mach
端口的状态,用常量表示,比如DISPATCH_MACH_SEND_DEAD
等。 - 自定义事件类型的
dispatch_source
,返回使用dispatch_source_merge_data
函数设置的数据。
- 继续监听
void
dispatch_resume(dispatch_object_t object);
- 挂起监听操作
void
dispatch_suspend(dispatch_object_t object);
-
dispatch_source_type_t type
:设置dispatch_source
方法的类型 -
uintptr_t handle
:取决于要监听的事件类型,比如如果是监听Mach
端口相关的事件,那么该参数就是mach_port_t
类型的Mach
端口号,如果是监听事件变量数据类型的事件那么该参数就不需要,设置为0就可以了。 -
unsigned long mask
:取决于要监听的事件类型 -
dispatch_queue_t _Nullable queue
:执行的队列,默认为全局队列
dispatch_source_type_t
的取值如下:
-
DISPATCH_SOURCE_TYPE_DATA_ADD
:属于自定义事件,可以通过dispatch_source_get_data
函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data
函数向dispatch_source
设置数据。 -
DISPATCH_SOURCE_TYPE_DATA_OR
:属于自定义事件,用法同DISPATCH_SOURCE_TYPE_DATA_ADD
。 -
DISPATCH_SOURCE_TYPE_MACH_SEND
:Mach
端口发送事件。 -
DISPATCH_SOURCE_TYPE_MACH_RECV
:Mach
端口接收事件。 -
DISPATCH_SOURCE_TYPE_PROC
:与进程相关的事件。 -
DISPATCH_SOURCE_TYPE_READ
:读文件事件。 -
DISPATCH_SOURCE_TYPE_WRITE
:写文件事件。 -
DISPATCH_SOURCE_TYPE_VNODE
:文件属性更改事件。 -
DISPATCH_SOURCE_TYPE_SIGNAL
:接收信号事件。 -
DISPATCH_SOURCE_TYPE_TIMER
:定时器事件。 -
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
:内存压力事件。
下面我们结合一个例子,具体的说明一下使用:
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger totalComplete;
- (void)initSource {
self.queue = dispatch_queue_create("soureQueue", 0);
// 创建soure事件
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
// 监听soure事件发生变化
dispatch_source_set_event_handler(self.source, ^{
// 获取source事件的值
NSUInteger value = dispatch_source_get_data(self.source);
self.totalComplete += value;
NSLog(@"进度:%.2f", self.totalComplete/100.0);
});
// 启动监听
dispatch_resume(self.source);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
for (NSUInteger index = 0; index < 100; index++) {
dispatch_async(self.queue, ^{
sleep(1);
// 设置source事件的数据
dispatch_source_merge_data(self.source, 1);
});
}
}
运行程序:
image总结
-
dispatch_once
- 会执行一次
- 线程安全
-
dispatch_after
是异步执行的 -
dispatch_apply
- 串行队列和普通循环相同
- 并发队列,循环的下标不是按顺序来的
-
dispatch_group
-
dispatch_group_enter
和dispatch_group_leave
必须成对出现,否则会造成死锁 - 先进后出,先
enter
后leave
-
dispatch_group_wait
会阻塞当前线程
-
-
dispatch_barrier
- 有同步的效果
- 性能安全
- 根本原理是堵塞队列
- 不要使用全局队列和主队列
- 拦截任务和栅栏函数需要是同一队列
-
dispatch_semaphore
- 起到锁的作用
- 是性能最高的锁
- 能够控制最大并发数
-
dispatch_semaphore_wait
的参数为0的时候会堵塞线程
-
dispatch_source
- 创建、监听回调、设置改变,形成了
dispatch_source
的基本操作 - 设置、接收数据的时候需要注意
source
的类型
- 创建、监听回调、设置改变,形成了
参考资料:
官方文档
iOS 多线程:『GCD』详尽总结