美文网首页iOS-机制
iOS-多线程(三)-GCD函数

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

作者: xxxxxxxx_123 | 来源:发表于2020-03-05 22:02 被阅读0次

    单次函数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));
    
    • iterations:执行迭代的次数
    • queue:执行迭代的队列,建议使用DISPATCH_APPLY_AUTO,会自动调用合适的线程队列
    • void (^block)(size_t)):迭代的结果回调
    image

    延迟函数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
    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)
    
    • 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);
    });
    

    运行程序,控制台输出:

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

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

    • 读文件类型的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); 
            });
        }
    }
    

    运行程序:

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

    相关文章

      网友评论

        本文标题:iOS-多线程(三)-GCD函数

        本文链接:https://www.haomeiwen.com/subject/inxlrhtx.html