ホームページ  >  記事  >  バックエンド開発  >  GoLang のコルーチンの詳細なグラフィックとテキストの説明

GoLang のコルーチンの詳細なグラフィックとテキストの説明

尚
転載
2019-11-28 14:14:583863ブラウズ

GoLang のコルーチンの詳細なグラフィックとテキストの説明

Coroutine は、Go 言語での軽量スレッド実装であり、Go ランタイムによって管理されます。

関数呼び出しの前に go キーワードを追加すると、呼び出しは新しい goroutine で同時に実行されます。呼び出された関数が戻ると、このゴルーチンも自動的に終了します。この関数が戻り値を持つ場合、戻り値は破棄されることに注意してください。

最初に次の例を見てください:

func Add(x, y int) {
    z := x + y
    fmt.Println(z)
}

func main() {
    for i:=0; i<10; i++ {
        go Add(i, i)
    }
}

上記のコードを実行すると、画面に何も表示されず、プログラムが終了することがわかります。

上記の例では、main() 関数は 10 個のゴルーチンを開始してから戻りますが、この時点でプログラムは終了し、Add() を実行する開始されたゴルーチンは実行する時間がありません。 main() 関数は、すべてのゴルーチンが終了するまで待ってから戻るようにしたいのですが、すべてのゴルーチンが終了したことをどのように確認すればよいでしょうか?これにより、複数のゴルーチン間の通信の問題が発生します。

エンジニアリングでは、共有メモリとメッセージという 2 つの最も一般的な同時通信モデルがあります。

次の例を見てください。10 個のゴルーチンが変数 counter を共有しています。各ゴルーチンが実行されると、カウンターの値が 1 ずつ増加します。10 個のゴルーチンが同時に実行されるため、ロックも導入します。コード内のロック変数。 main()関数では、forループを使用してカウンタ値を継続的に確認し、その値が10に達するとすべてのゴルーチンが実行されたことを意味し、この時点でmain()はリターンしてプログラムを終了します。

package main
import (
    "fmt"
    "sync"
    "runtime"
)

var counter int = 0

func Count(lock *sync.Mutex) {
    lock.Lock()
    counter++
    fmt.Println("counter =", counter)
    lock.Unlock()
}


func main() {

    lock := &sync.Mutex{}

    for i:=0; i<10; i++ {
        go Count(lock)
    }

    for {
        lock.Lock()

        c := counter

        lock.Unlock()

        runtime.Gosched()    // 出让时间片

        if c >= 10 {
            break
        }
    }
}

上記の例では、ロック変数 (共有メモリの一種) を使用してコルーチンを同期していますが、実際、Go 言語では主にメッセージ メカニズム (チャネル) が通信モデルとして使用されます。

channel

メッセージ メカニズムは、各同時実行ユニットが自己完結型の独立した個体であり、独自の変数を持っているとみなします。これらの変数は、異なる同時ユニット間では共有されません。各同時ユニットには、メッセージという入力と出力が 1 つだけあります。

チャネルとはGo言語が言語レベルで提供するゴルーチン間の通信方法であり、チャネルを利用して複数のゴルーチン間でメッセージを受け渡すことができます。チャネルはプロセス内通信メソッドであるため、チャネルを介してオブジェクトを渡すプロセスは、関数呼び出し時のパラメータ受け渡し動作と一致します。たとえば、ポインタも渡すことができます。
チャネルは型に関連しています。チャネルは 1 つの型の値のみを渡すことができます。この型はチャネルを宣言するときに指定する必要があります。

チャネルの宣言形式は次のとおりです:

var chanName chan ElementType

たとえば、int 型を渡すチャネルを宣言します:

var ch chan int

組み込み関数 make() を使用して、チャネルを定義する:

ch := make(chan int)

チャネルの使用法で最も一般的なものには、書き込みと読み取りが含まれます:

// 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据
ch <- value

// 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止
value := <-ch

デフォルトでは、相手側の準備が整わない限り、チャネルの送受信はブロックされます。 。 良い。

バッファ付きチャネルを作成することもできます:

c := make(chan int, 1024)

// 从带缓冲的channel中读数据
for i:=range c {
  ...
}

現時点では、サイズ 1024 の int 型のチャネルを作成します。リーダーがいない場合でも、ライターはいつでもアクセスできます。バッファがいっぱいになるまで、チャネルへの書き込みはブロックされません。

使用されなくなったチャネルは閉じることができます:

close(ch)

チャネルはプロデューサーの場所で閉じる必要があります。コンシューマーの場所で閉じられると、簡単にパニックが発生します。

閉じたチャネル上の受信操作 (

ここでチャネルを使用して上記の例を書き換えます:

func Count(ch chan int) {
    ch <- 1
    fmt.Println("Counting")
}

func main() {

    chs := make([] chan int, 10)

    for i:=0; i<10; i++ {
        chs[i] = make(chan int)
        go Count(chs[i])
    }

    for _, ch := range(chs) {
        <-ch
    }
}

この例では、10 個のチャネルを含む配列が定義され、配列内の各チャネルが 10 個の異なるゴルーチンに割り当てられます。各ゴルーチンが完了すると、データがゴルーチンに書き込まれ、チャネルが読み取られるまでこの操作はブロックされます。

すべてのゴルーチンが開始された後、10 チャネルから順にデータが読み取られますが、この操作も、対応するチャネルがデータを書き込む前にブロックされます。このように、チャネルはロックのような関数を実装するために使用され、すべてのゴルーチンが完了した後にのみ main() が返されるようにします。

さらに、チャネル変数を関数に渡すとき、一方向チャネル変数として指定することで、関数内でこのチャネルの操作を制限できます。

一方向チャネル変数の宣言:

var ch1 chan int      // 普通channel
var ch2 chan <- int    // 只用于写int数据
var ch3 <-chan int    // 只用于读int数据

型変換を通じてチャネルを一方向に変換できます:

ch4 := make(chan int)
ch5 := <-chan int(ch4)   // 单向读
ch6 := chan<- int(ch4)  //单向写

一方向チャネルの役割は、 c に似ています。 in の const キーワードは、コード内の「最小特権の原則」に従うために使用されます。

たとえば、関数内で一方向読み取りチャネルを使用する場合:

func Parse(ch <-chan int) {
    for value := range ch {
        fmt.Println("Parsing value", value) 
    }
}

ネイティブ タイプとして、次のストリーミング処理構造のように、チャネル自体をチャネル経由で渡すこともできます。

type PipeData struct {
    value int
    handler func(int) int
    next chan int
}

func handle(queue chan *PipeData) {
    for data := range queue {
        data.next <- data.handler(data.value)
    }
}

select

UNIX では、select() 関数を使用して記述子のグループを監視します。このメカニズムは実装によく使用されます。同時実行性の高いソケット、サーバー プログラム。 Go 言語は、非同期 IO 問題に対処するために使用される select キーワードを言語レベルで直接サポートしています。一般的な構造は次のとおりです:

select {
    case <- chan1:
    // 如果chan1成功读到数据
    
    case chan2 <- 1:
    // 如果成功向chan2写入数据

    default:
    // 默认分支
}

select はデフォルトでブロックされており、存在する場合にのみ発生します。監視対象チャネルでの送信または受信。実行中、複数のチャネルの準備ができたら、select はランダムに 1 つを選択して実行します。

Go语言没有对channel提供直接的超时处理机制,但我们可以利用select来间接实现,例如:

timeout := make(chan bool, 1)

go func() {
    time.Sleep(1e9)
    timeout <- true
}()

switch {
    case <- ch:
    // 从ch中读取到数据

    case <- timeout:
    // 没有从ch中读取到数据,但从timeout中读取到了数据
}

这样使用select就可以避免永久等待的问题,因为程序会在timeout中获取到一个数据后继续执行,而无论对ch的读取是否还处于等待状态。

并发

早期版本的Go编译器并不能很智能的发现和利用多核的优势,即使在我们的代码中创建了多个goroutine,但实际上所有这些goroutine都允许在同一个CPU上,在一个goroutine得到时间片执行的时候其它goroutine都会处于等待状态。

实现下面的代码可以显式指定编译器将goroutine调度到多个CPU上运行。

import "runtime"...
runtime.GOMAXPROCS(4)

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

GoLang のコルーチンの詳細なグラフィックとテキストの説明

调度

Go调度的几个概念:

M:内核线程;

G:go routine,并发的最小逻辑单元,由程序员创建;

P:处理器,执行G的上下文环境,每个P会维护一个本地的go routine队列;

GoLang のコルーチンの詳細なグラフィックとテキストの説明

 除了每个P拥有一个本地的go routine队列外,还存在一个全局的go routine队列。

具体调度原理:

1、P的数量在初始化由GOMAXPROCS决定;

2、我们要做的就是添加G;

3、G的数量超出了M的处理能力,且还有空余P的话,runtime就会自动创建新的M;

4、M拿到P后才能干活,取G的顺序:本地队列>全局队列>其他P的队列,如果所有队列都没有可用的G,M会归还P并进入休眠;

一个G如果发生阻塞等事件会进行阻塞,如下图:

GoLang のコルーチンの詳細なグラフィックとテキストの説明

G发生上下文切换条件:

系统调用;

读写channel;

gosched主动放弃,会将G扔进全局队列;

如上图,一个G发生阻塞时,M0让出P,由M1接管其任务队列;当M0执行的阻塞调用返回后,再将G0扔到全局队列,自己则进入睡眠(没有P了无法干活);

以上がGoLang のコルーチンの詳細なグラフィックとテキストの説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。