草庐IT

iOS 多线程原理 - GCD函数底层

顶级蜗牛 2023-05-23 原文

libdispatch-1271.120.2 下载
苹果官方资源opensource

多线程相关文献:
iOS 多线程原理 - 线程与队列底层
iOS 多线程原理 - GCD函数底层
iOS 线程底层 - 锁

本章节探究:
1.单例 dispatch_once
2.栅栏函数 barrier
3.调度组 group
4.信号量 semaphore
5.dispatch_source

前言

在了解了线程与队列的底层原理之后,本章节来看看GCD函数的底层原理,研究这些API是怎么调用的,并附上使用案例。

一、单例

+ (SingleExample *)shareInstance {
    static SingleExample *single = nil;
    static dispatch_once_t onceToken ;
    dispatch_once(&onceToken, ^{
        single = [[SingleExample alloc] init];
    }) ;
    return single;
}

来看看dispatch_once这个函数原理。
打开libdispatch源码

dispatch_once的源码声明:

dispatch_once

dispatch_once_t *val它里面有一个状态的记录,来保证block只被调用一次。

dispatch_once_f的源码声明:

dispatch_once_f

需要看看执行func的要出于什么条件下才会被执行 (_dispatch_once_gate_tryenter

_dispatch_once_gate_tryenter的源码声明:

_dispatch_once_gate_tryenter

而等待是怎么等的呢?(_dispatch_once_wait
_dispatch_once_wait的源码声明:

在死循环里不断地查询单例状态,一旦任务执行完毕才跳出循环。

单例总结:
1.线程安全
2.任务只会被执行一次
3.通过一个状态值来保证任务是否被执行过

二、栅栏函数

同步栅栏函数dispatch_barrier_sync案例:

- (void)test_barrier {
    dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 栅栏函数
    dispatch_barrier_sync(t, ^{
        sleep(2);
        NSLog(@"%@", [NSThread currentThread]); // main
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}
// 12顺序不一定;3一定在12后面;45在3后面;45顺序不一定
// 同步栅栏dispatch_barrier_sync 和 普通的同步dispatch_sync效果是一样的

异步栅栏函数dispatch_barrier_async案例:

- (void)test_barrier {
    dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 异步栅栏函数
    dispatch_barrier_async(t, ^{
        sleep(2);
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}
// 124顺序不一定,3一定在12后面,5一定在3后面
// 异步栅栏函数只能栅得住非全局队列的任务

全局队列案例

- (void)test_barrier_global {
    dispatch_queue_t t = dispatch_get_global_queue(0, 0);
    dispatch_async(t, ^{
        NSLog(@"1");
    });
    dispatch_async(t, ^{
        NSLog(@"2");
    });
    // 栅栏函数
    dispatch_barrier_async(t, ^{
        sleep(2);
        NSLog(@"3");
    });
    
    NSLog(@"4");
    
    dispatch_async(t, ^{
        NSLog(@"5");
    });
}

// 1245没有顺序 3在最后
// 异步栅栏函数栅不住全局队列里的任务

栅栏函数分为同步栅栏异步栅栏
dispatch_barrier_async在自定义的并发队列里,全局和串行达不到我们要的效果。
苹果文档中指出,如果使用的是全局队列或者创建的不是并发队列,则dispatch_barrier_async实际上就相当于dispatch_async

1.同步栅栏dispatch_barrier_sync

其实同步栅栏与普通同步实现的效果是差不多的,在源码上只有一点点小差异。

dispatch_barrier_sync
_dispatch_barrier_sync_f
_dispatch_barrier_sync_f_inline

_dispatch_barrier_sync_f_inline里会判断不同的队列条件来去选择分支继续往下走,这里我以并发队列为例,它会走_dispatch_sync_f_slow代码分支:

_dispatch_sync_f_slow

以并发队列为例,它会走_dispatch_sync_invoke_and_complete_recurse代码分支:

_dispatch_sync_invoke_and_complete_recurse

_dispatch_sync_function_invoke_inline和dispatch_sync底层一样的,是去调用func
栅栏函数完成任务后会执行_dispatch_sync_complete_recurse唤醒队列里后续的任务。

_dispatch_sync_complete_recurse

_dispatch_sync_complete_recurse里通过do...while去唤醒队列里的任务dx_wakeup
dx_wakeup是一个dq_wakeup的宏定义:

#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
dq_wakeup

(栅栏函数栅不住全局队列的原因就在这里,因为它指定的wakeup函数不一样。)

唤醒以并发队列为例,它会走_dispatch_lane_wakeup

_dispatch_lane_wakeup

为barrier形式,调用_dispatch_lane_barrier_complete

_dispatch_lane_barrier_complete
  • 如果是串行队列,则会进行等待,等待其他的任务执行完成,再按顺序执行;
  • 如果是并发队列,则会调用_dispatch_lane_drain_non_barriers方法将栅栏之前的任务执行完成;
  • 最后会调用_dispatch_lane_class_barrier_complete方法,也就是把栅栏拔掉了,不拦了,从而执行栅栏之后的任务。

唤醒以全局并发队列为例,它会走_dispatch_root_queue_wakeup
它里面就没有拦截有关栅栏函数相关的东西。

_dispatch_root_queue_wakeup

同步栅栏函数dispatch_barrier_sync和普通同步函数dispatch_sync效果是一样的:
阻塞当前线程,不开辟线程,立即执行,同步栅栏函数还需要等栅栏任务完成后唤醒非全局队列后续的任务

为什么苹果设计栅栏函数栅不住全局并发队列?
因为我们系统也会使用全局并发队列,避免造成系统任务被阻塞。

2.异步栅栏dispatch_barrier_async
dispatch_barrier_async
  • _dispatch_continuation_init保存任务
_dispatch_continuation_init

_dispatch_continuation_init保存了任务,在需要执行的时候拿出来执行

_dispatch_continuation_init
  • _dispatch_continuation_async
_dispatch_continuation_async

dx_push是宏定义:

#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

找到dq_push的声明:

dq_push

根据不同的队列赋值给dq_push不一样的函数
以并发队列为例:

_dispatch_lane_concurrent_push的源码声明:

_dispatch_lane_concurrent_push就是栅栏异步与普通异步函数的分支:

_dispatch_lane_concurrent_push
_dispatch_lane_push

走到dx_wakeup函数,这里在同步栅栏部分已经介绍过了。

栅栏函数总结:
1.栅栏函数只针对非全局队列;
2.栅栏函数不能栅住全局队列,因为系统也在用它,防止阻塞住系统任务;
3.栅栏函数需要等待当前队列前面的任务执行完,再去执行栅栏任务,最后唤醒执行栅栏任务后面的任务

三、调度组

- (void)test_group {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t que1 = dispatch_queue_create("An", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t que2 = dispatch_queue_create("Lin", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_enter(group);
    dispatch_async(que1, ^{
        sleep(4);
        NSLog(@"1");
        dispatch_group_leave(group);
    });

    dispatch_group_enter(group);
    dispatch_async(que2, ^{
        sleep(3);
        NSLog(@"2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
        sleep(2);
        NSLog(@"3");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_main_queue(), ^{
        sleep(1);
        NSLog(@"4");
        dispatch_group_leave(group);
    });

    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"6");
    });

    dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"5");
    });
}
// 5在4321任务之后

当然也可以使用dispatch_group_async来代替dispatch_group_enterdispatch_group_leave;效果是一样的。

研究的对象有三个:dispatch_group_enterdispatch_group_leavedispatch_group_notify

1. dispatch_group_enter的源码分析
dispatch_group_enter

ps: 这里 DISPATCH_GROUP_VALUE_INTERVAL = 0x0000000000000004ULL
注释里说的 0->-1 跃迁的进位其实是位运算。实际上是+4

苹果官方文档dispatch_group_enter的解释:
调用此函数将增加组中当前未完成任务的计数。如果应用程序通过dispatch_group_async函数以外的方式显式地从组中添加和删除任务,那么使用这个函数(与dispatch_group_leave一起使用)允许您的应用程序正确地管理任务引用计数。对这个函数的调用必须与对dispatch_group_leave的调用相平衡。您可以使用此函数同时将一个块与多个组关联。

2. dispatch_group_leave的源码分析
dispatch_group_leave

苹果官方文档dispatch_group_leave的解释:
调用此函数将减少组中当前未完成任务的计数。如果应用程序通过dispatch_group_async函数以外的方式显式地从组中添加和删除任务,那么使用这个函数(与dispatch_group_enter一起使用)允许您的应用程序正确地管理任务引用计数。
对该函数的调用必须平衡对dispatch_group_enter的调用。调用它的次数超过dispatch_group_enter是无效的,这会导致负的计数。

3.dispatch_group_notify的源码分析
dispatch_group_notify
dispatch_group_notify

dispatch_group_enter通过改变调度组状态值+4;dispatch_group_leave通过调度组状态值-4;dispatch_group_notify在调度组内不断地获取调度组状态值,如果状态值达到平衡(等于0),则说明前面的任务做完了,需要执行notify里的任务。

调度组总结:
1.dispatch_group_enterdispatch_group_leave必须成对使用;
2.dispatch_group_leave次数多于dispatch_group_enter会导致崩溃;
3.调度组底层是通过修改调度组的状态值的增(enter)减(leave),不断地监听这个状态值是否达到平衡(等于0),一旦平衡则去执行dispatch_group_notify里的任务。

四、信号量

- (void)test_semaphore {
    // 设置0,任务1不需要等;设置1,任务1和2不需要等...
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1");
        dispatch_semaphore_signal(sem); 
    });
    
    // 等待任务1的signal
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        NSLog(@"2");
        dispatch_semaphore_signal(sem);
    });
    
     // 等待任务2的signal
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"3");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"4");
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"5");
        dispatch_semaphore_signal(sem);
    });
}
// 12345
  • dispatch_semaphore_create 创建信号量,指定信号量数值
  • dispatch_semaphore_signal 发送信号量,将信号量数值+1
  • dispatch_semaphore_wait 等待信号量;当信号量数值为0时,阻塞当前线程一直等待;当信号量数值大于等于1时,将信号量数值-1并执行当前线程的任务
1.dispatch_semaphore_create的源码声明:
dispatch_semaphore_signal声明
dispatch_semaphore_signal实现
  • 当两个线程需要协调特定事件的完成时,为该值传递0很有用;
  • 传递大于0的值对于管理有限的资源池很有用,其中池大小等于该值;
  • 信号量的起始值传递小于信号量的起始值。 传递小于零的值将导致返回 NULL,也就是小于0就不会正常执行

总的来说:信号量初始值可以控制线程池中的最多并发数量

2.dispatch_semaphore_signal的源码声明:
dispatch_semaphore_signal

os_atomic_inc2o原子操作自增加1,然后会判断,如果value > 0,就会返回0;
加一次后依然小于0就报异常 Unbalanced call to dispatch_semaphore_signal(),然后会调用_dispatch_semaphore_signal_slow做容错的处理。

_dispatch_semaphore_signal_slow
_dispatch_sema4_signal
3. dispatch_semaphore_wait的源码声明:
dispatch_semaphore_wait
  • os_atomic_dec2o进行原子自减1操作,也就是对value值进行减操作,控制可并发数。
  • 如果可并发数为2,则调用该方法后,变为1,表示现在并发数为 1,剩下还可同时执行1个任务,不会执行_dispatch_semaphore_wait_slow去等待。
  • 如果初始值是0,减操作之后为负数,则会调用_dispatch_semaphore_wait_slow方法。

看看_dispatch_semaphore_wait_slow的等待逻辑

_dispatch_semaphore_wait_slow
_dispatch_sema4_wait

一个do-while循环,当不满足条件时,会一直循环下去,从而导致流程的阻塞。
上面举例里面就相当于,下图中的情况:

举例

信号量总结:
1.dispatch_semaphore_wait 信号量等待,内部是对并发数做自减1操作,如果小于0,会执行_dispatch_semaphore_wait_slow然后调用_dispatch_sema4_wait是一个do-while,直到满足条件结束循环。
2.dispatch_semaphore_signal 信号量释放 ,内部是对并发数做自加1操作,直到大于0时,为可操作。
3.保持线程同步,将异步执行任务转换为同步执行任务。
4.保证线程安全,为线程加锁,相当于自旋锁。

五、dispatch_source

  • dispatch_source_create 创建源
  • dispatch_source_set_event_handler 设置源事件回调
  • dispatch_source_merge_data 源事件设置数据
  • dispatch_source_get_data 获取源事件数据
  • dispatch_resume 继续
  • dispatch_suspend 挂起
  • dispatch_source_cancel 取消源事件

定时器监听
倒计时案例:

- (void)iTimer {
    __block int timeout = 60;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        if(timeout <= 0) {
            dispatch_source_cancel(_timer);
        } else {
            timeout--;
            NSLog(@"倒计时:%d", timeout);
        }
    });
    dispatch_resume(_timer);
}

自定义事件,变量增加
变量增加案例:

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIButton *iBt;
@property (weak, nonatomic) IBOutlet UIProgressView *iProgress;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger totalComplete;
@property (nonatomic ,assign) int iNum;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.totalComplete = 0;
    self.queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_event_handler(self.source, ^{
        NSUInteger value = dispatch_source_get_data(self.source); // 每次去获取iNum的值
        self.totalComplete += value;
        NSLog(@"进度: %.2f",self.totalComplete/100.0);
        self.iProgress.progress = self.totalComplete/100.0;
    });
    
//    [self iTimer];
}

- (IBAction)btClick:(id)sender {
    if ([self.iBt.titleLabel.text isEqualToString:@"开始"]) {
        dispatch_resume(self.source);
        NSLog(@"开始了");
        self.iNum = 1;
        [sender setTitle:@"暂停" forState:UIControlStateNormal];
        
        for (int i= 0; i<1000; i++) {
            dispatch_async(self.queue, ^{
                sleep(1);
                dispatch_source_merge_data(self.source, self.iNum); // 传递iNum触发hander
            });
        }
    } else {
        dispatch_suspend(self.source);
        NSLog(@"暂停了");
        self.iNum = 0;
        [sender setTitle:@"开始" forState:UIControlStateNormal];
    }
}
@end

附上dispatch_source的Demo

有关iOS 多线程原理 - GCD函数底层的更多相关文章

  1. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  4. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  5. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  6. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  7. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  8. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  9. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  10. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

随机推荐