首頁  >  文章  >  後端開發  >  單線是go語言的特性嗎

單線是go語言的特性嗎

青灯夜游
青灯夜游原創
2023-01-06 11:17:193394瀏覽

單執行緒不是go語言的特性,go語言是多執行緒的。 Golang的線程模型是MPG模型,整體上Go程與內核線程是多對多對應的,因此Go一定是多線程模式的;其中M與內核線程是1比1對應,然後多個G與多個M對應,P指的是上下文資源。

單線是go語言的特性嗎

本教學操作環境:windows7系統、GO 1.18版本、Dell G3電腦。

單執行緒不是go語言的特性,go語言是多執行緒的。 單線的話還好意思號稱多核心時代為高並發而生的語言?

Golang 的線程模型是 M P G 模型,整體上 Go 程與內核線程是多對多對應的,因此首先來講就一定是多線程的。其中 M 與核心執行緒是1比1對應,然後多個 G 與多個 M 對應,P 指的是上下文資源,不多說。 M 的數量(或者說內核線程)什麼時候會增加呢?就是在目前的 M 數量無法調度得動目前所有 G 的時候就起新的 M 來處理。

Go 並發(多執行緒)

有人把Go比喻成21世紀的C語言,第一名是因為Go語言設計簡單,第二,21世紀最重要的是平行程式設計,而Go從語言層面就支持了並行。

goroutine

#goroutine是Go平行設計的核心。 goroutine說到底其實就是線程,但是它比線程更小,十幾個goroutine可能體現在底層就是五、六個線程,Go語言內部幫你實現了這些goroutine之間的記憶體共享。執行goroutine只需極少的棧記憶體(大概是4~5KB),當然會根據對應的資料伸縮。也因為如此,可同時運行成千上萬個並發任務。 goroutine比thread更易用、更有效率、更輕。

goroutine是透過Go的runtime管理的一個執行緒管理器。 goroutine透過go關鍵字實現了,其實就是一個普通的函數。

go hello(a, b, c)

透過關鍵字go就啟動了一個goroutine。我們來看一個例子

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") //开一个新的Goroutines执行
    say("hello") //当前Goroutines执行
}

// 以上程序执行后将输出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

我們可以看到go關鍵字很方便的就實現了並發程式設計。上面的多個goroutine運行在同一個進程裡面,共享記憶體數據,不過設計上我們要遵循:不要透過共享來通信,而要透過通信來共享。

runtime.Gosched()表示讓CPU把時間片讓給別人,下次某個時候繼續恢復執行該goroutine。

預設情況下,調度器只使用單線程,也就是說只實現了並發。想要發揮多核心處理器的並行,需要在我們的程式中明確呼叫 runtime.GOMAXPROCS(n) 告訴調度器同時使用多個執行緒。 GOMAXPROCS 設定了同時運行邏輯程式碼的系統執行緒的最大數量,並傳回先前的設定。如果n < 1,不會改變目前設定。以後Go的新版本中調度得到改進後,這將被移除。

channels

#goroutine運行在相同的位址空間,因此存取共享記憶體必須做好同步。那麼goroutine之間如何進行數據的通訊呢,Go提供了一個很好的通訊機制channel。 channel可以與Unix shell 中的雙向管道做類比:可以透過它發送或接收值。這些值只能是特定的類型:channel類型。定義一個channel時,也需要定義傳送到channel的值的類型。注意,必須使用make 建立channel:

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channel透過運算子<-來接收和傳送資料

ch <- v    // 发送v到channel ch.
v := <-ch  // 从ch中接收数据,并赋值给v

我們把這些應用到我們的例子中來:

package main

import "fmt"

func sum(a []int, c chan int) {
    total := 0
    for _, v := range a {
        total += v
    }
    c <- total  // send total to c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c  // receive from c

    fmt.Println(x, y, x + y)
}

預設情況下,channel接收和發送資料都是阻塞的,除非另一端已經準備好,這樣就使得Goroutines同步變的更加的簡單,而不需要顯式的lock。所謂阻塞,也就是如果讀取(value := <-ch)它將會被阻塞,直到有資料接收。其次,任何發送(ch<-5)將會被阻塞,直到資料被讀出。無緩衝channel是在多個goroutine之間同步很棒的工具。

Buffered Channels

#上面我們介紹了預設的非快取類型的channel,不過Go也允許指定channel的緩衝大小,很簡單,就是channel可以儲存多少元素。 ch:= make(chan bool, 4),創造了可以儲存4個元素的bool 型channel。在這個channel 中,前4個元素可以無阻塞的寫入。當寫入第5個元素時,程式碼將會阻塞,直到其他goroutine從channel 讀取一些元素,騰出空間。

ch := make(chan type, value)

value == 0 ! 无缓冲(阻塞)
value > 0 ! 缓冲(非阻塞,直到value 个元素)

我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的value值

package main

import "fmt"

func main() {
    c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行
    c <- 1
    c <- 2
    fmt.Println(<-c)
    fmt.Println(<-c)
}
    //修改为1报如下的错误:
    //fatal error: all goroutines are asleep - deadlock!

Range和Close

上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 1, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x + y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

for i := range c能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过内置函数close关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。

记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的

Select

我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

package main

import "fmt"

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

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

select {
case i := <-c:
    // use i
default:
    // 当c阻塞的时候执行这里
}

超时

有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现:

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
                case v := <- c:
                    println(v)
                case <- time.After(5 * time.Second):
                    println("timeout")
                    o <- true
                    break
            }
        }
    }()
    <- o
}

runtime goroutine

runtime包中有几个处理goroutine的函数:

  • Goexit

    退出当前执行的goroutine,但是defer函数还会继续调用

  • Gosched

    让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

  • NumCPU

    返回 CPU 核数量

  • NumGoroutine

    返回正在执行和排队的任务总数

  • GOMAXPROCS

    用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

【相关推荐:Go视频教程编程教学

以上是單線是go語言的特性嗎的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn