Golang 之 协程 goroutine(二)

版权声明:本文为博主原创文章,转载文章须附上文章来源与作者。 https://blog.csdn.net/ChangerJJLee/article/details/81151071

“子程序就是协程的一种特例。” - - Donald Knuth

这里写图片描述

  • 普通函数,一个线程内有个main函数调一个叫doSomeWork的函数,等doSomeWork做完以后才会将控制权交还给main函数,然后main函数执行下一个语句

  • 协程,main和doSomeWork之间有个双向通道,数据与控制权可以双向流通,可能放在一个或者多个线程里

这里写图片描述

抢占式多任务处理

  • 计算机只有一个处理器,宏观上,我们却可以看到数以百计的线程正同时运行在机器上,这就是抢占式多任务处理的效果,通过操作系统内核通过对线程的调度(调度器作为内核的一部分),将时间切片,分成一段段的时间片。这些时间间隔以毫秒为精度且长度并不固定。针对每个处理器,每个时间片仅服务于单独一个线程。线程的迅速执行给我们造成了它们在同时运行的假象。我们在两个时间片的间隔中进行上下文切换。该方法的优点在于,那些正在等待某些操作系统资源的线程将不会浪费时间片,直到资源有效为止。
  • 在此种方式下线程将被系统强制性中断。在上下文切换的过程中,操作系统会在下一个线程将要执行的代码中插入一条跳转到下一个上下文切换的指令。该指令是一个软中断,如果线程在遇到这条指令前就终止了(例如,它正在等待某个资源),那么该指定将被删除而上下文切换也将提前发生。

  • 抢占式多任务处理的主要缺点在于,必须使用一种同步机制来保护资源以避免它们被无序访问。除此之外,还有另一种多任务管理模型,被称为协调式多任务管理,其中线程间的切换将由线程自己负责完成。该模型普遍认为太过危险,原因在于线程间的切换不发生的风险太大。但Windows操作系统仅仅实现了抢占式多任务处理

协程:轻量级线程

  • 非抢占式多任务处理,由协程主动交出控制权,因此才能做到轻量级(没有过多的上下文状态的保存)

  • 在golang中,协程只是编译器,解释器层面的多任务(调度器调度协程)

  • 多个协程可能在一个或多个线程上运行

10000个协程并发输出,printf有IO操作,IO操作时协程会释放控制权,因此可以看到10000并发输出,开的线程数不会超过核心数。

package main

import (
    "fmt"
    "time"
)

func main()  {
    for i:= 0; i<10000; i++ {
        go func(j int) {
            for {
                fmt.Printf("goroutine from %d\n", j)
            }
        }(i)
    }
    // main 与 go 后面的立即执行函数是并发执行,此处让main函数等待10分钟 ,防止main先执行完退出
    // 从而杀掉 go 后面的立即函数
    time.Sleep(time.Minute*10)
}

无IO并发,无控制权释放

package main

import (
    "fmt"
    "time"
)

func main()  {

    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {
        go func(j int) {
            for {
                a[j]++
            }
        }(i)
    }

    time.Sleep(time.Minute*10)
    fmt.Println(a)
}
//结果:
//[0 0 0 0 0 0 0 0 0 0]

这里写图片描述

手动交出控制权。10个协程都能获得控制权,做自增运算

"""  goroutine.go """
package main

import (
    "time"
    "runtime"
    "fmt"
)

func main()  {


    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {
        // 传值,防止闭包,导致index out of range
        go func(j int) {
            for {
                a[j]++
                // 手动交出控制权
                runtime.Gosched()
            }
        }(i)
    }

    time.Sleep(time.Second * 3)
    fmt.Println(a)
}

//结果:
//[0 0 0 0 0 0 0 0 0 0]
//[729611 707997 728874 704561 709436 705376 708068 703098 710653 711052]
  • go run -race goroutine.go,此处26行与18行有读写会有异常,但不会报错,当在fmt.Println(a),读的时候,a[j]++可能正在写,这就需要channel来解决问题
==================
WARNING: DATA RACE
Read at 0x00c042092000 by main goroutine:
  main.main()
      E:/GOProject/src/10_骞惰/goroutine.go:26 +0x1ec

Previous write at 0x00c042092000 by goroutine 6:
  main.main.func1()
      E:/GOProject/src/10_骞惰/goroutine.go:18 +0x72

Goroutine 6 (running) created at:
  main.main()
      E:/GOProject/src/10_骞惰/goroutine.go:16 +0x1b3
==================
[729591 717311 717799 722817 721530 722732 721418 722303 717897 713184]
Found 1 data race(s)
exit status 66

此处注意因闭包而产生的报错问题

package main

import (
    "time"
    "fmt"
)

func main()  {

    var a [10]int
    fmt.Println(a)
    for i:= 0; i<10; i++ {

        go func() {
            a[i]++
        }()
    }

    time.Sleep(time.Second * 3)
    fmt.Println(a)
}
// panic: 
// runtime error: index out of range

其他语言

  • C++:Boost.Coroutine
  • Java:原生不支持,第三JVM有解决方案
  • Python:yield关键字模拟协程, Python 3.5:async def 对协程原生支持

定义goroutine,任何函数只需加上go就能送给调度器运行,不需要再定义时区分是异步函数(不同于Python 3.5:async),调度器在合适的点切换

这里写图片描述

合适的点切换(仅供参考)

  • IO,select
  • channel
  • 等待锁
  • 函数调用(有时),有调度器决定
  • runtime.Gosched

猜你喜欢

转载自blog.csdn.net/ChangerJJLee/article/details/81151071