Golang 中goroutine的理解

CSP模型  

  Golang中通过CSP(communicating sequential processes)模型来通信,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。Golang中channel 是被单独创建并且可以在进程之间传递,它的通信模式类似于 boss-worker 模式的,一个实体通过将消息发送到channel 中,然后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是一定要被另外的实体消费掉的,在实现原理上其实类似一个阻塞的消息队列。

Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程,Golang底层选择使用coroutine的出发点是因为,

它具有以下特点:

  • 用户空间 避免了内核态和用户态的切换导致的成本.
  • 可以由语言和框架层进行调度.
  • 更小的栈空间允许创建大量的实例.

Golang 调度器GPM原理与调度分许

Golang 中正确理解context

在完全理解goroutine前可以尝试先阅读这两篇文章

golang在语言层面支持协程,goroutine的切换和管理不再依赖于系统的线程与进程,也不依赖于cpu核心数量,由golang运行时统一管理。在一些其他语言中,主要通过库的方式支持协程,仅支持一些简单的(创建,销毁,切换等功能),在这种方式生成的协程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行的协程,从而无法达到轻量级线程本身期望达到的目标

goroutine的生命周期:

  goroutine 以非阻塞的方式执行,它们会随着程序(主线程)的结束而消亡,它们的生命周期取决于创建它们的线程,goroutine之间是不存在父子关系,在goroutine中创建新的goroutine,父goroutine结束不影响子roroutine继续运行,他们应该同属于某一个线程


父子协程的生命周期问题:

func main(){
    print("main start\n")
    go func() {
        print("父goroutine start\n")
        go func() {
            for {
                print("子goroutine start\n")
                timer := time.NewTimer(time.Second * 1)
                <-timer.C
            }
        }()
        time.Sleep(time.Second*2)
        print("父goroutine finish\n")
    }()
    time.Sleep(time.Second*5)
    print("main finish\n")
}

main start
父goroutine start
子goroutine start
子goroutine start
父goroutine finish
子goroutine start
子goroutine start
子goroutine start
main finish

输出结果显示子goroutine是不受父goroutine控制的

如果想通过父goroutine控制子goroutine,那么就需要context

func main(){
    print("main start\n")
    go func() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel()
        print("父goroutine start\n")
        go func() {
            for {
                select {
                case <-ctx.Done():
                    print("子goroutine have to finish\n")
                    return
                default:
                    print("子goroutine start\n")
                    timer := time.NewTimer(time.Second * 1)
                    <-timer.C
                }
            }
        }()
        time.Sleep(time.Second*2)
        print("父goroutine finish\n")
    }()
    time.Sleep(time.Second*5)
    print("main finish\n")
}

输出

main start
父goroutine start
子goroutine start
子goroutine start
父goroutine finish
子goroutine have to finish
main finish

goroutine与线程的区别

从系统调度的角度,线程由系统内核去调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用内核调度函数,终止正在运行的线程,把该线程的寄存器信息保存到内存当中,再从内存恢复某一个线程的注册表信息,继续执行选中的线程。这种OS级别的线程切换需要完整的上下文切换过程,即保存一个线程的状态到内存中,再从内存中恢复另一个线程的状态,再更新调度器的数据结构,这个过程相对耗时。goroutine的切换由processor管理,不需要由时钟中断来控制,也不用切换到内核语境,开销非常小

从栈空间来看,系统线程分配固定的2MB栈空间,用于存储函数调用期间,其他函数的局部变量,goroutine栈空间分配2KB,其可以按需增大和缩小,最大限制可以到1GB

使用goroutine要注意防止内存泄漏

  goroutine 泄露发生时,该 goroutine 的栈(一般 2k 内存空间起)一直被占用不能释放,goroutine 里的函数在堆上申请的空间也不能被 垃圾回收器 回收。这样,在程序运行期间,内存占用持续升高,可用内存越来也少,最终将导致系统崩溃。

  常见的泄漏场景有往channel写没有读,导致阻塞(channel还有很多中阻塞的情况),select中阻塞,for循环不退出

  可以通过一些代码习惯避免这些泄漏,比如select,可以用定时器解决阻塞,也可以用context解决阻塞

  

func fibonacci(c chan int, ctx context.Context)  {
    x, y := 0, 1
    for{
        select {
        case c <- x:
            x, y = y, x+y
        case <-ctx.Done():
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    c := make(chan int)

    go fibonacci(c, ctx)

    for i := 0; i < 10; i++{
        fmt.Println(<- c)
    }

    cancel()

    time.Sleep(5 * time.Second)
}

 

posted @ 2019-11-16 23:29  LeeJuly  阅读(1147)  评论(0)    收藏  举报