iOS-多线程(三)-GCD函数

多线程(一)-原理
多线程(二)-GCD基础
多线程(三)-GCD函数
多线程(四)-GCD定时器安全

单次函数dispatch_once

单次函数通常用来建立单例或者是执行只须要执行一次的程序。bash

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

延迟函数的做用是在指定的队列中,按照给定的时间执行一个操做。app

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:async

  1. 建立调度组
dispatch_group_t dispatch_group_create(void);
复制代码
  1. 进组,开始执行组内任务
void dispatch_group_enter(dispatch_group_t group);
复制代码
  1. 出组,组任务执行完成
void dispatch_group_leave(dispatch_group_t group);
复制代码
  1. 同步等待,阻塞当前线程直到组的任务都执行完成或者timeout归零才会继续下一步
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
复制代码
  1. 组所关联的全部任务已经完成,发出一个通知告知
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=");
});
复制代码

运行程序,控制台输出:post

能够看出这并非咱们想要的结果。对程序进行修改,继续运行:

一样使用dispatch_group_wait也会获得相应的结果:

可是dispatch_group_wait会阻塞以后的操做,好比咱们在组通知以后还执行了NSLog(@"==5=="),组任务并无阻塞到它的执行,而dispatch_group_wait就会阻塞。

注意,dispatch_group_enterdispatch_group_leave必须成对出现,不然会形成死锁。

栅栏函数dispatch_barrier

栅栏函数分为dispatch_barrier_asyncdispatch_barrier_sync函数,这两个函数既有共同点,又有不一样点:

  • 共同点:
  1. 等待在它前面插入队列的任务先执行完
  2. 等待他们本身的任务执行完再执行后面的任务
  • 不一样点:
  1. dispatch_barrier_sync将本身的任务插入到队列的时候,须要等待本身的任务结束以后才会继续插入被写在它后面的任务,而后执行它们
  2. 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--");
    });
}
复制代码

运行程序:

dispatch_barrier_async函数改成dispatch_barrier_sync,而后运行程序:

经过打印结果能够看出栅栏函数不论是同步异步,都会对当前队列中的任务起到隔离做用,就是会让栅栏以前的多线程操做先执行,让栅栏以后的多线程操做后执行。不一样的是dispatch_barrier_async函数以后的多线程操做都是并发执行,而dispatch_barrier_sync以后的操做都是同步执行,因此咱们打印的barrierAsync的执行顺序和barrierSync不一样。

简而言之,dispatch_barrier_syncdispatch_barrier_async都会隔离队列中栅栏先后的任务,不一样的是会不会阻塞当前队列。因此栅栏函数和其拦截的任务必须是同一队列的,否则没有阻塞效果。因此在AFN中使用栅栏函数没有效果,AFN本身维护了一个串行队里,除非使用这个队列才会起做用。

注意,当咱们在主线程中调用任务,并且将同步栅栏函数也添加到主队列中,会发生死锁现象。使用栅栏函数要使用自定义队列,防止阻塞、死锁。

信号量dispatch_semaphore_t

一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问以前,加上信号量的处理,则可告知系统按照咱们指定的信号量数量来执行多个线程。

相关API:

  1. 建立信号量,参数:信号量的初值,若是小于0则会返回NULL,该参数控制当前能开启的线程数量。
dispatch_semaphore_t dispatch_semaphore_create(long value)
复制代码
  1. 等待(减小)信号量,信号出现以后才会返回。
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,超时会返回其余值

  1. 发信号(增长信号量)。若是以前的值小于零,该函数会唤醒等待的线程
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);
});
复制代码

运行程序,控制台输出:

将建立的信号量改成2:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
复制代码

将建立的信号量改成3,或者大于3:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
复制代码

同理,咱们还能够将例子中的并发任务改成同步任务。能够得出以下结论:

  • 若是是同步任务,无论建立的信号量和任务数的关系,都是按照顺序一个接一个执行
  • 若是是异步任务:
    • 建立的信号量小于任务数,就会先按照信号量的数量执行相应的任务,剩下任务会等到以前执行的任务执行完成才会接着执行
    • 建立的信号量大于等于任务数,全部任务都会并发执行

再来看一个例子:

__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

  1. 建立源
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
	uintptr_t handle,
	unsigned long mask,
	dispatch_queue_t _Nullable queue);
复制代码
  1. 设置源事件回调
void
dispatch_source_set_event_handler(dispatch_source_t source,
	dispatch_block_t _Nullable handler);
复制代码
  1. 设置源事件数据
void
dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
复制代码
  1. 获取源事件数据
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函数设置的数据。
  1. 继续监听
void
dispatch_resume(dispatch_object_t object);
复制代码
  1. 挂起监听操做
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_SENDMach端口发送事件。
  • DISPATCH_SOURCE_TYPE_MACH_RECVMach端口接收事件。
  • 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); 
        });
    }
}
复制代码

运行程序:

总结

  1. dispatch_once
    • 会执行一次
    • 线程安全
  2. dispatch_after是异步执行的
  3. dispatch_apply
    • 串行队列和普通循环相同
    • 并发队列,循环的下标不是按顺序来的
  4. dispatch_group
    • dispatch_group_enterdispatch_group_leave必须成对出现,不然会形成死锁
    • 先进后出,先enterleave
    • dispatch_group_wait会阻塞当前线程
  5. dispatch_barrier
    • 有同步的效果
    • 性能安全
    • 根本原理是堵塞队列
    • 不要使用全局队列和主队列
    • 拦截任务和栅栏函数须要是同一队列
  6. dispatch_semaphore
    • 起到锁的做用
    • 是性能最高的锁
    • 可以控制最大并发数
    • dispatch_semaphore_wait的参数为0的时候会堵塞线程
  7. dispatch_source
    • 建立、监听回调、设置改变,造成了dispatch_source的基本操做
    • 设置、接收数据的时候须要注意source的类型

参考资料:
官方文档
iOS 多线程:『GCD』详尽总结

相关文章
相关标签/搜索