Golang - 并发与函数字面量 - go, chan & func
-
并发机制:go的并发编程模型是基于消息(通道)的。比如,下述代码中的斐波那契数列,如果主线程(协程)来维护的话,就需要将a与b从局部变量升级为全局变量,但丢给子协程处理的话,主线程(协程)就不必为a和b操心了,这样的子协程即为一个generator,而主线程(协程)与generator子协程之间的通信则是通过一个channel。再比如,下述代码中的素数筛,每取一个素数,就将这个素数用来筛前一个generator生成的候选数字,以生成下一个generator,每个generator都是一个协程,维护各自的素数筛,而其素数筛即为上一个generator抛出的第一个数字。P.S. Linux文件标准输入输出的通道,与此类似,文件按行读取文本,而go协程则从通道中读取一个个对象。
package qqiwei // go test -timeout 30s -run ^TestChannelPrime$ qiwei\try -v // TestChannelFib: Fibonacci Number implemented through channel // TestGlobalFibNormal: Fibonacci Number implemented through global/static variable // TestChannelPrime: Prime Number implemented through channel import ( "testing" ) func TestChannelFib(t *testing.T) { for i := range fibGenerator() { t.Log(i) if i >= 8 { break } } } func fibGenerator() <-chan int { res := make(chan int) go func() { // local, or static? a, b := 0, 1 for { a, b = b, a+b res <- a } }() return res } func TestGlobalFibNormal(t *testing.T) { for { i := fibNormal() t.Log(i) if i >= 8 { break } } } var a, b = 0, 1 func fibNormal() int { // package level a, b a, b = b, a+b return a } func primeGenerator() <-chan int { res := make(chan int) // 2 -> // 3 3 -> // 4 // 5 5 5 -> // 6 // 7 7 7 // 9 9 go func() { for curChannel := func() <-chan int { naturalNumber := make(chan int) go func() { for i := 2; ; i++ { naturalNumber <- i } }() return naturalNumber }(); ; { curPrimeNumber := <-curChannel res <- curPrimeNumber curChannel = func(curChan <-chan int) <-chan int { nextChan := make(chan int) go func() { for { if i := <-curChan; i%curPrimeNumber != 0 { nextChan <- i } } }() return nextChan }(curChannel) } }() return res } func TestChannelPrime(t *testing.T) { for i := range primeGenerator() { if i > 13 { break } t.Log(i) } }
-
闭包(Go/C++/Java):
-
在写上面这段代码的时候,踩过两个误区,其中一个是关于闭包的。在Golang中,闭包就是函数字面值,而函数字面值就是匿名函数。如下,是go文档中对闭包的定义,简洁明了。重点是,闭包就是共享内存,而不论它是否在像其他传统C系语言那样的局部中被定义,只要它被用到的地方,它的生命就存在;也不论它是结构体,还是primitive type,都是做引用,即,这是一份共享内存。Go之所以有这样的自由度,就是因为存在从栈到堆的内存逃逸机制,一个局部变量分配在栈上还是堆上是编译器来识别管理的。而C++非但受限于局部auto变量的生命周期与其作用域一致(事实就是默认的函数栈机制,btw. go的函数栈也是动态大小的,不会栈溢出)外,其拷贝捕获与传参存在事实上的概念冗余。而Go更加分明,传参就是值拷贝,数组(非切片)也照样逐个拷贝,而捕获就是事实上的引用。有了这个自由度,就不再需要顾及太多,对象是完全共享的,并发通信是通过channel实现的,起一个协程很easy。既然,闭包的意义在于简化可调用对象的实现,使得函数拥有自己的状态(C/C++的局部static变量是一种原始的方式),那么,C++/Java对引用捕获的限制,就是一种严重的不足。另外,对于golang来说,
:=
(和var
)之所以从赋值符=
区别出来,就在于此时,这个变量(对象)才有了生命,有了内存;而在C++/Java中,初始化与赋值依然没有完全被区分开地共用着一个符号,这是上古的C语言所引入的头文件式的声明与定义分离编译所引入的麻烦。Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
--- https://go.dev/ref/spec#Function_literals -
在C++中,lambda表达式就是一个匿名的可调用对象,其类型为局部类型(局部仅起命名空间/作用域之用,与Java局部类事实上以其外部类的某个实例对象为其构造实参/属性不同),因为是匿名的,该可调用对象的初始化是伴随lambda表达式而发生的。引用捕获的lambda表达式在其被调用处,被捕获的引用是否存在,与引用一样是由人为确保的。如果是在栈上,虽然对其的计算可能比new于堆上的对象快,但对其的引用在离开函数作用域就失效了。在排除这个问题后,C++还存在一个问题,就是默认来说,被捕获的对象在lambda表达式中是const的,即该可调用对象持有被捕获对象为其一个const属性;需要加mutable才可去掉这个事实上的成员变量的const性质。本质上,一切问题,都没有逃出const &的概念之外。
#include <iostream> int main() { int ci = 100; auto cf = [=]() { return ci; }; std::cout << cf(); ci += 1; std::cout << cf() << std::endl; int cj = 100; auto cg = [&]() { return cj; }; std::cout << cg(); cj += 1; std::cout << cg() << std::endl; int i = 100; auto f = [=]() mutable { return i++; }; std::cout << f(); i += 1; std::cout << f() << std::endl; int j = 100; auto g = [&]() mutable { return j++; }; std::cout << g(); j += 1; std::cout << g() << std::endl; }
-
Java被闭包的对象必须是事实上的常量,不论在lambda表达式内部还是在其外围作用域内,被捕获的自由变量都不能编辑,毫不自由。但是,Java的final往往仅仅指无法指向其他对象(不像C++对象的const是作为方法匹配条件的),所以,可以用一个AtomicReference来持有实际的局部对象,并用get/set方法来修改被闭包引用的指向。Java的内部类与lambda表达式在被闭包的对象是事实上的常量这一点上是一致的。内部类功能更丰富,而且仅仅是一种编译器合成的类,而lambda表达式则是一种动态方法。此外,内部类还具有很多丰富的功能,比如,可以用一个匿名局部类来获取外围类的信息,比如,实现一种懒加载且并发安全的单例模式。P.S. 类中静态字段的初始化,直接初始化赋值和静态块不同,后者由一个初始化函数完成。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
-
-
defer是有助于理解go函数的一个完美的例子。注意,defer表达式与其调用的函数之间有区别。deferred func的参数在入deferred func栈的时候,也就是在defer表达式执行的时候,就计算了。只是在其外围函数即将返回,返回值也已经装填好了之后,先进后出地执行deferred func。中间出现panic,后面弹出的deferred func也会挨个执行完。而闭包则是一种引用。
package main import ( "fmt" "unsafe" ) func innerTestDefer() (res int) { res = -1 defer fmt.Println("bye") var funcs [4]func() for i := 0; i < 3; i++ { fmt.Printf("i: %d, i_address: 0x%x\n", i, &i) // 0 1 2 defer fmt.Printf("closure i: %d, &i: 0x%x\n", i, &i) defer func(i int) { fmt.Printf("param i: %d, &i: 0x%x\n", i, &i) res = i }(i) funcs[i] = func() { // function literal fmt.Printf("funcs[%d] i: %d, &i: 0x%x\n", i, i, &i) } } var i_address uintptr fmt.Print("i_address: ") fmt.Scanf("0x%x", &i_address) i_pointer := (*int)(unsafe.Pointer(i_address)) *i_pointer = 4 for i := 0; i < 3; i++ { funcs[i]() } // defer funcs[3]() // nullptr panic defer fmt.Printf("done\n\n") return func() int { fmt.Println("return") return 5 }() } func main() { fmt.Println("innerTestDefer", innerTestDefer()) }