Golang - 并发与函数字面量 - go, chan & func

  1. 并发机制: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)
        }
    }
    
    
  2. 闭包(Go/C++/Java):

    1. 在写上面这段代码的时候,踩过两个误区,其中一个是关于闭包的。在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

    2. 在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;
      }
      
      
    3. 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; 
          }  
      }
      
  3. 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())
    }
    
    
posted @ 2022-07-23 23:11  joel-q  阅读(105)  评论(2编辑  收藏  举报