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)
チャネルはプロデューサーの場所で閉じる必要があります。コンシューマーの場所で閉じられると、簡単にパニックが発生します。
閉じたチャネル上の受信操作 (<-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的函数,
调度
Go调度的几个概念:
M:内核线程;
G:go routine,并发的最小逻辑单元,由程序员创建;
P:处理器,执行G的上下文环境,每个P会维护一个本地的go routine队列;
除了每个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如果发生阻塞等事件会进行阻塞,如下图:
G发生上下文切换条件:
系统调用;
读写channel;
gosched主动放弃,会将G扔进全局队列;
如上图,一个G发生阻塞时,M0让出P,由M1接管其任务队列;当M0执行的阻塞调用返回后,再将G0扔到全局队列,自己则进入睡眠(没有P了无法干活);
以上がGoLang のコルーチンの詳細なグラフィックとテキストの説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。