iOS-机制

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

2020-03-05  本文已影响0人  xxxxxxxx_123

单次函数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也可以保证线程安全。

image

迭代函数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));
image

延迟函数dispatch_after

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

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t block);
image

调度组函数dispatch_group

通过Dispatch Group,我们可以将多个任务放入一个组中,并且可以让他们在同一队列或不同队列上异步执行,执行完成之后,再执行其他的依赖于这些任务的操作。

相关API:

  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=");
});

运行程序,控制台输出:

image

可以看出这并不是我们想要的结果。对程序进行修改,继续运行:

image

同样使用dispatch_group_wait也会得到相应的结果:

image

但是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--");
    });
}

运行程序:

image

dispatch_barrier_async函数改为dispatch_barrier_sync,然后运行程序:

image

通过打印结果可以看出栅栏函数不管是同步异步,都会对当前队列中的任务起到隔离作用,就是会让栅栏之前的多线程操作先执行,让栅栏之后的多线程操作后执行。不同的是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)
  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);
});

运行程序,控制台输出:

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

  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);

获取的数据类型和源事件的类型相关:

  1. 继续监听
void
dispatch_resume(dispatch_object_t object);
  1. 挂起监听操作
void
dispatch_suspend(dispatch_object_t object);

dispatch_source_type_t的取值如下:

下面我们结合一个例子,具体的说明一下使用:

@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

总结

  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』详尽总结

上一篇下一篇

猜你喜欢

热点阅读