Block底层实现原理

古巷悠悠岁月深,青石老街印旧痕今夜小楼听风雨,不见当年伞下人 前言Block作为iOS中老生常谈的问题,也是面试中面试官比较喜欢问的 一个问题 ,下面我们通过源码查看block的底层实现原理 什么是BlockBlock:将函数及其上下文组装起来的对象 Block本质就是一个对象创建一个PHJBlock类 @implementation PHJBlock - (void)test { int a = 10; void (^ block)(void) = ^{ NSLog(@"%d", a); }; block(); } @end查看编译后的C++源码 编译: Clang -rewrite-objc PHJBlock.m static void _I_PHJBlock_test(PHJBlock * self, SEL _cmd) { int a = 10; void (* block)(void) = ((void (*)())&__PHJBlock__test_block_impl_0((void *)__PHJBlock__test_block_func_0, &__PHJBlock__test_block_desc_0_DATA, a)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } __PHJBlock__test_block_impl_0的内部实现 struct __PHJBlock__test_block_impl_0 { struct __block_impl impl; struct __PHJBlock__test_block_desc_0* Desc; int a; __PHJBlock__test_block_impl_0(void *fp, struct __PHJBlock__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };__block_impl的内部实现 ...

July 15, 2019 · 3 min · jiezi

iOS:Block __block修饰符

__block修饰符上一篇文章中说过,auto类型的局部变量,可以被block捕获,但是不能修改值。__block可以解决block内部无法修改外部auto变量的问题。__block int age = 10;void (^myblock)(void) = ^{ NSLog(@"%d",age);};age = 20;myblock();用法就是这么简单,这样我们修改age为20的时候,打印也是20。我们看看编译后的代码。struct __Block_byref_age_0 { void *__isa;__Block_byref_age_0 __forwarding; int __flags; int __size; int age;};//Blockstruct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __Block_byref_age_0 *age; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 desc, __Block_byref_age_0 _age, int flags=0) : age(_age->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};在block内部多了一个指向__Block_byref_age_0类型结构体的age指针。上面我也帖上了这个__Block_byref_age_0结构体的结构。我们发现int类型的age在这个结构体内部了。那也就是说,__block修饰的变量,编译器会把它包装成一个对象,然后我们的这个成员变量放到了这个对象的内部。我们观察一下这个__Block_byref_age_0内部,这些变量可能有疑惑的也就是这个__forwarding。他是一个指向这个结构体自身的指针。而且我们还可以看出来在打印age的时候,是也是通过__forwarding调用的age(age->__forwarding->ag),具体为什么要多加这个字段,我们后面再说。static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (copy)(struct __main_block_impl_0, struct __main_block_impl_0); void (dispose)(struct __main_block_impl_0);}__main_block_desc_0这个结构体中也多了两个指针,这是与内存管理有关的函数。底层分析差不多了,那我们还没说到为什么__block修饰的属性,在block内部可以修改,我们看下面的代码 attribute((blocks(byref))) __Block_byref_age_0 age = {(void)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};void (myblock)(void) = ((void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));(age.__forwarding->age) = 20;我们创建了__Block_byref_age_0类型的age对象,同时把外部age的值也就是10,传递了进去。然后初始化了block。关键是下面的修改age的值的时候,直接就是修改的age对象里面的age属性了,然后打印的时候,也是打印的他。这个地方其实还是挺抽象的了,也不是很好理解。怎么前面定义的age变量跟后面修改的就不是一个了?__block int age = 10;NSLog(@"%p",&age);void (^myblock)(void) = ^{ NSLog(@"%d",age);};NSLog(@"%p",&age);age = 20;myblock();这是最简单的方法,打印出两个age的地址,就是不一样的。那我们怎么去判断就是__Block_byref_age_0里面的age呢,大家可以参考下面的做法。struct __Block_byref_age_0 { void *__isa; struct __Block_byref_age_0 *__forwarding; int __flags; int __size; int age;};struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(void); void (*dispose)(void);};struct __block_impl { void *isa; int Flags; int Reserved; void FuncPtr;};struct __main_block_impl_0 { struct __block_impl impl;// 8+4+4+8 struct __main_block_desc_0 Desc;//8 struct __Block_byref_age_0 *age;// 8+8+4+4};int main(int argc, const char * argv[]) { @autoreleasepool { _block int age = 10; void (^block)(void) = ^{ age = 20; NSLog(@“age is %d”, age); }; struct main_block_impl_0 *blockImpl = (bridge struct main_block_impl_0 *)block; NSLog(@"%p", &age); } return 0;}上面的操作就是我们把底层的一些结构拿出来,然后把我们的block类型桥接成__main_block_impl_0类型。然后我们通过debug可以拿到这个blockImpl的地址,然后通过内存中的地址偏移计算出来内部__Block_byref_age_0中的age地址,看看和打印出来的age地址是否一致。我们拿到blockImpl的地址是0x1005002d0(我测试时候的值,每次都不同)。main_block_impl_0内部第一个属性是一个__block_impl结构体,然后__block_impl内部两个指针(一个指针8字节)两个int(一个int4字节),共占24字节。第二个参数是一个指针,8字节。age在第三个参数指向的结构体中,也就是说__Block_byref_age_0类型的age内存地址是0x1005002d0偏移32,也就是0x100500300。然后__Block_byref_age_0内部的age变量前面,有两个指针两个int,24字节,0x100500300再偏移24,也就是0x100500318。跟我们NSLog(@"%p", &age);打印的一致。所以可以得出,我们修改的这个age,其实就是底层age对象内部的age变量。上面我们留下了一个__forwarding指针的疑问,我们先不着急解决,先说说block类型。block的类型block有isa指针,开始我想通过写了几个类型的block,用clang编译看cpp代码,但发现一直是这个样子impl.isa = &NSConcreteStackBlock;。所以我就用最直接的打印[obj class]的方法。注意,因为在ARC的环境下,编译器给我们做了很多内存相关的工作,所以我在研究block类型的过程中切换到了MRC环境。我用过的例子就不写了,下面是一个小总结。一共有三种Block__NSGlobalBlock 内存位于数据区__NSStackBlock 栈区__NSMallocBlock 堆区具体什么样的block对应哪一种类型?NSGlobalBlock:没有访问auto变量__NSStackBlock:访问了auto变量__NSMallocBlock: __NSStackBlock__调用copy提示我们在声明一个block属性的时候,习惯用copy关键字,是为了把栈区的block拷贝到堆区,让我们来管理他的生命周期。ARC环境下会根据情况自动将栈上的block拷贝到堆上。ARC环境下也用copy是为了和MRC环境统一,也可以用strong。__block修饰对象类型当__block变量在栈上时,不会对指向的变量产生强引用。当__block变量copy到堆上时,会根据这个变量的修饰符是__strong,__weak,__unsafe_unretained做出相应的操作形成强引用(retain)或者弱引用。(ARC会retain,MRC不会)。__forwarding指针最后说一下上面的__forwarding指针问题。这个图可以很好地诠释这个问题了。我们的block在内存中可能位于栈上,可能在堆上。使用了这个指针之后,让我们在block位于不同内存位置的情况下,访问到相应准确位置的变量。 ...

April 7, 2019 · 1 min · jiezi

iOS:Block 循环引用问题

循环引用是一个比较常见的问题,之前面试的时候也会被问到,如何解决循环引用问题,其实大家都知道使用__block,__weak这些修饰符可以解决循环引用问题,那今天我们要讨论的就是他们是怎么样解决了循环引用问题的。__weak其实__weak是比较好理解的,它的作用就是在两方相互强引用的时候,把其中一个引用变为弱引用,打破这个循环引用的圈。我们通过代码看一下。MyPerson * person = [[MyPerson alloc] init];person.age = @“10”;__weak typeof(person) weakPerson = person;person.block = ^{ NSLog(@“age is %@”, weakPerson.age);}; MyPerson类里面有一个block,一个string类型的age,在执行block的时候,打印了age,如果不用weakPerson的话,就会产生循环引用,这种用法想必大家都很熟悉。那我们看一下编译后的cpp文件。struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; MyPerson *__weak weakPerson; __main_block_impl_0(void fp, struct __main_block_desc_0 desc, MyPerson __weak _weakPerson, int flags=0) : weakPerson(_weakPerson) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};可以看到block内部捕获到的是MyPerson __weak weakPerson;,所以不会产生强引用,自然也就不会出现循环引用问题。__weak只在ARC环境下使用。__block最开始我以为__block消除循环引用的方式跟__weak是一样的。//这种用法ARC环境下是错的 MRC可以MyPerson * person = [[MyPerson alloc] init];person.age = @“10”; __block typeof(person) weakPerson = person;person.block = ^{ NSLog(@“age is %@”, weakPerson.age);};我们现在开发一直都是在ARC环境下,首先自己检讨一下,我一直都以为__block可以这么用,而且关键是这样用了确实编译器就没有了关于循环引用的警告了。但是我们如果重写一下MyPerson类的dealloc方法,让对象释放时打印点东西,你会发现如果使用__weak,在main函数结束时,person会调用dealloc释放,但是如果像上面一样用__block,person不会释放,还是存在循环引用。我强调了这种用法在ARC环境下不可以,但是在MRC环境下是可以的,因为MRC环境下block不会对__block修饰的属性强引用。下面是ARC环境正确的__block使用方式。如果就按照__weak的使用方法使用,在block内部把weakPerson置为nil,同时这个block必须要调用。MyPerson * person = [[MyPerson alloc] init];person.age = @“10”; __block typeof(person) weakPerson = person;person.block = ^{ NSLog(@“age is %@”, weakPerson.age); weakPerson = nil;};person.block();//必须有这个代码也可以这样写:__block MyPerson * person = [[MyPerson alloc] init];person.age = @“10”;person.block = ^{ NSLog(@“age is %@”, person.age); person = nil;}; person.block();两种写法都一样,必须手动置为nil,然后必须执行block。下面我们说一下原理。首先呢,我们上面已经说了,如果不手动置为nil的话,使用__block依然有循环引用,我们结合cpp的代码分析一下具体循环引用在什么地方。我们编译下面这种写法。MyPerson * person = [[MyPerson alloc] init];__block typeof(person) weakPerson = person;person.age = @“10”;person.block = ^{ NSLog(@“age is %@”, weakPerson.age);};我们来分析一下下面的代码struct __Block_byref_weakPerson_0 { void __isa;__Block_byref_weakPerson_0 __forwarding; int __flags; int __size; void (__Block_byref_id_object_copy)(void, void); void (__Block_byref_id_object_dispose)(void); typeof (person) weakPerson;};struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __Block_byref_weakPerson_0 *weakPerson; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};其实这个__block属性的作用咱们之前已经说过了,就是在block内部把修饰的属性包装成一个对象,也就是这个__Block_byref_weakPerson_0。__Block_byref_weakPerson_0内部有我们的weakPerson属性,typeof (person) weakPerson,这里只是叫weakPerson,他的持有方式还是strong的。所以说我们可以分析出来,__block属性持有我们的person变量,person持有block,block内部持有这个__block属性,就像下面这个图示一样。我们通过置为nil解决它循环引用的方式,就是打断一条强引用。如下图__unsafe_unretainedARC环境下__unsafe_unretained与__weak使用方法相同。MRC环境下,与__block MRC环境下的使用一样。__unsafe_unretained和__weak对比:__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil__unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变 ...

April 7, 2019 · 1 min · jiezi

iOS:Block变量捕获

这篇博客我们从一个很常见的题目入手。int age = 10; void (^myblock)(void) = ^{ NSLog(@"%d",age);};age = 20;myblock();这个题目就涉及到了block内访问外部变量,block有个变量捕获机制,我们新建一个mac的命令行工程,把上面代码写进去,然后用clang把main.m文件编译为cpp的文件看一下。具体的block底层结构上一篇文章我们已经说过了,这里我们针对结构就不在赘述,直接说核心点。auto类型局部变量struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};可以看到,在block的内部,多了一个age变量。而且从初始化的函数来看,这个__main_block_impl_0中age的值是外面传进来赋给他的。我们看一下main函数中。 int age = 10;void (myblock)(void) = ((void ()())&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA, age));age = 20;((void ()(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);可以看出来在这个block初始化的过程中,就把age的值,也就是10,传了进去,赋值给了block内部的age变量。那我再看一下打印的时候的age是什么情况。static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int age = __cself->age; // bound by copy NSLog((NSString )&__NSConstantStringImpl__var_folders_6l_80sfw0tn35bg4dxc51jlq_bw0000gn_T_main_3b5bf4_mi_0,age);}打印的这个age就是block自己内部的这个ege变量(值为10),所以我们在执行block之前,改变外面的age值为20,其实改变的不是内部要打印的这个age了,所以打印出来结果还是10。好的,我们定义的这个age就是一个普通的局部变量,其实就是auto类型的局部变量(C语言基础)。那我们下面改变一下这个变量的属性,尝试一下静态变量(static)和全局变量。static类型局部变量首先是静态变量。//定义变量时加static标识static int age = 10;运行结果是20。我们依然要看一下clang编译一下,看c++代码。struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; int *age; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};我们来分析一下两次的异同,首先block都对变量进行了捕获,但不同的是static类型的变量是捕获了变量的指针,那我们应该就可以理解了,一般情况下这种基本数据类型如果是传指针,就意味要修改值的。我们从main函数中也可以看出来,在初始化block的时候传入了外部age的指针,static int age = 10;void (myblock)(void) = ((void ()())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &age));而且在打印的时候也是打印了这个指针指向的数据。static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int *age = __cself->age; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_6l_80sfw0tn35bg4dxc51jlq_bw0000gn_T_main_d618bf_mi_0,(age));}所以我们在执行block之前更改了age的值,在执行block的时候打印的也是同一块内存的值,所以值改变了。最后我们在试一下全局变量。全局变量在定义age的时候,定义成一个全局变量。(拿到main方法外面)。int age = 10;struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};从编译后的代码可以看出,在底层其实也是生成了一个age=10的全局变量,然后在block内部并没有捕获这个变量。在不管是重新赋值,还是输出打印,都是操作的这个全局的age,所以这个值也是能被改变的。可能有人会问如果是static int age = 10;这个样子的全局变量呢?全局变量加不加static都是一样的。这里就不上代码细说了。小结局部变量:会被block捕获到内部auto类型的是值传递,内部不能修改值static是指针传递,可以修改值。全局变量:不会被捕获到block内部可以修改值上面我们使用的是基本数据类型,那对象是不是也一样呢?其实对象类型的也是一样的,但是有一个小点我们需要了解一下。看下面这个例子吧。NSMutableArray * arr = [NSMutableArray new]; void (^myblcok)(void) = ^{ [arr addObject:@“1”]; NSLog(@"%@",arr);}; myblcok();按照我们上面总结的这种auto类型的局部变量是要被捕获到内部的,但是应该不可以修改值。上面代码执行后打印数组,是有@“1”这个元素的,这里我们就要搞清楚,我们调用[arr addObject:@“1”];并没有修改arr的值,只是只用了这个指针。如果我们在block的回调中让arr=nil;,这算是改变arr的值,但是xcode就会报错的了。 ...

April 7, 2019 · 1 min · jiezi

iOS:Block的本质

我们项目中经常使用block来进行回调传值,之前我对block的认识也就仅仅的停留在基础的层面,包括简单的使用和一些基本的避免循环引用的方法,这篇博客是我在对block进行了更深一层的学习之后的记录和总结,希望对大家有所帮助。Block的本质新建一个命令行项目,写一个简单的block如下面所示。void (^myBlock)(void) = ^{ NSLog(@“11”);}; myBlock();使用clang工具把main.m文件编译为cpp文件。main函数变成了下面这个样子int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; void (myBlock)(void) = ((void ()())&__main_block_impl_0((void )__main_block_func_0, &__main_block_desc_0_DATA)); ((void ()(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl )myBlock); } return 0;}里面有很多类型转换的代码,但是不难看出,我们的block被编译成了一个__main_block_impl_0类型的变量,我们可以搜索一下,这是一个结构体。struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0 Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};这个__main_block_impl_0结构体有两个属性,一个是__block_impl类型的结构体,一个是指向__main_block_desc_0结构体的指针。我们一个一个的看。//__block_impl 结构体struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr;};我们可以看到这个结构体有一个isa指针,同时在__main_block_impl_0这个结构体中是直接包含__block_impl结构体,从内存结构中其实这个isa指针就在__main_block_impl_0里面,也就是说block被编译成了一个拥有isa指针的结构体。了解过isa的应该知道,这算是oc对象的一个标志了,那也就是说block其实也是一个oc的对象。我们继续看__main_block_impl_0内部 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }这是一个初始化的方法,因为这是c++代码嘛,c语言的结构体应该是不可以这样写的。只要是给这个__block_impl类型的impl赋值,这里我们先主要看这个fp指针。从编译之后main函数中看出来,在初始化__main_block_impl_0的时候传进去的参数是一个__main_block_func_0。// __main_block_func_0static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_zx_pr_dx33d329d7xxgk9kgjdw80000gp_T_main_85af40_mi_0);}可以看出来这是我们block回调执行的方法,也就是说impl.FuncPtr指向了block的回调。__main_block_impl_0还有一个Desc参数,我们可以在文件中查一下。static struct __main_block_desc_0 { size_t reserved; size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};reserved是一个保留字段,Block_size就是记录我们这个block占内存的大小。本节小结Block本质就是一个OC对象,内部有isa指针。Block是封装了函数调用和函数调用的环境的OC对象。我们来个草图来梳理一下最简单的block的内部结构。 ...

April 4, 2019 · 1 min · jiezi

Golang并发:一招掌握无阻塞通道读写

介绍Golang并发的模型写了几篇了,但一直没有以channel为主题进行介绍,今天就给大家聊一聊channel,channel的基本使用非常简单,想必大家都已了解,所以直接来个进阶点的:介绍channel的阻塞情况,以及给你一个必杀技,立马解决阻塞问题,实用性高。阻塞场景无论是有缓存通道、无缓冲通道都存在阻塞的情况。阻塞场景共4个,有缓存和无缓冲各2个。无缓冲通道的特点是,发送的数据需要被读取后,发送才会完成,它阻塞场景:通道中无数据,但执行读通道。通道中无数据,向通道写数据,但无协程读取。// 场景1func ReadNoDataFromNoBufCh() { noBufCh := make(chan int) <-noBufCh fmt.Println(“read from no buffer channel success”) // Output: // fatal error: all goroutines are asleep - deadlock!}// 场景2func WriteNoBufCh() { ch := make(chan int) ch <- 1 fmt.Println(“write success no block”) // Output: // fatal error: all goroutines are asleep - deadlock!}注:示例代码中的Output注释代表函数的执行结果,每一个函数都由于阻塞在通道操作而无法继续向下执行,最后报了死锁错误。有缓存通道的特点是,有缓存时可以向通道中写入数据后直接返回,缓存中有数据时可以从通道中读到数据直接返回,这时有缓存通道是不会阻塞的,它阻塞场景是:通道的缓存无数据,但执行读通道。通道的缓存已经占满,向通道写数据,但无协程读。// 场景1func ReadNoDataFromBufCh() { bufCh := make(chan int, 1) <-bufCh fmt.Println(“read from no buffer channel success”) // Output: // fatal error: all goroutines are asleep - deadlock!}// 场景2func WriteBufChButFull() { ch := make(chan int, 1) // make ch full ch <- 100 ch <- 1 fmt.Println(“write success no block”) // Output: // fatal error: all goroutines are asleep - deadlock!}使用Select实现无阻塞读写select是执行选择操作的一个结构,它里面有一组case语句,它会执行其中无阻塞的那一个,如果都阻塞了,那就等待其中一个不阻塞,进而继续执行,它有一个default语句,该语句是永远不会阻塞的,我们可以借助它实现无阻塞的操作。如果不了解,不想多了解一下select可以先看下这2篇文章:Golang并发模型:轻松入门selectGolang并发模型:select进阶下面示例代码是使用select修改后的无缓冲通道和有缓冲通道的读写,以下函数可以直接通过main函数调用,其中的Ouput的注释是运行结果,从结果能看出,在通道不可读或者不可写的时候,不再阻塞等待,而是直接返回。// 无缓冲通道读func ReadNoDataFromNoBufChWithSelect() { bufCh := make(chan int) if v, err := ReadWithSelect(bufCh); err != nil { fmt.Println(err) } else { fmt.Printf(“read: %d\n”, v) } // Output: // channel has no data}// 有缓冲通道读func ReadNoDataFromBufChWithSelect() { bufCh := make(chan int, 1) if v, err := ReadWithSelect(bufCh); err != nil { fmt.Println(err) } else { fmt.Printf(“read: %d\n”, v) } // Output: // channel has no data}// select结构实现通道读func ReadWithSelect(ch chan int) (x int, err error) { select { case x = <-ch: return x, nil default: return 0, errors.New(“channel has no data”) }}// 无缓冲通道写func WriteNoBufChWithSelect() { ch := make(chan int) if err := WriteChWithSelect(ch); err != nil { fmt.Println(err) } else { fmt.Println(“write success”) } // Output: // channel blocked, can not write}// 有缓冲通道写func WriteBufChButFullWithSelect() { ch := make(chan int, 1) // make ch full ch <- 100 if err := WriteChWithSelect(ch); err != nil { fmt.Println(err) } else { fmt.Println(“write success”) } // Output: // channel blocked, can not write}// select结构实现通道写func WriteChWithSelect(ch chan int) error { select { case ch <- 1: return nil default: return errors.New(“channel blocked, can not write”) }}使用Select+超时改善无阻塞读写使用default实现的无阻塞通道阻塞有一个缺陷:当通道不可读或写的时候,会即可返回。实际场景,更多的需求是,我们希望尝试读一会数据,或者尝试写一会数据,如果实在没法读写再返回,程序继续做其它的事情。使用定时器替代default可以解决这个问题,给通道增加读写数据的容忍时间,如果500ms内无法读写,就即刻返回。示例代码修改一下会是这样:func ReadWithSelect(ch chan int) (x int, err error) { timeout := time.NewTimer(time.Microsecond * 500) select { case x = <-ch: return x, nil case <-timeout.C: return 0, errors.New(“read time out”) }}func WriteChWithSelect(ch chan int) error { timeout := time.NewTimer(time.Microsecond * 500) select { case ch <- 1: return nil case <-timeout.C: return errors.New(“write time out”) }}结果就会变成超时返回:read time outwrite time outread time outwrite time out示例源码本文所有示例源码,及历史文章、代码都存储在Github:https://github.com/Shitaibin/…这篇文章了channel的阻塞情况,以及解决阻塞的2种办法:使用select的default语句,在channel不可读写时,即可返回使用select+定时器,在超时时间内,channel不可读写,则返回希望这篇文章对你的channel读写有所启发。如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2018/11/03/Golang-channel-read-and-write-without-blocking/ ...

December 27, 2018 · 2 min · jiezi