Heim  >  Artikel  >  Backend-Entwicklung  >  Detaillierte Grafik- und Texterklärung von Coroutinen in GoLang

Detaillierte Grafik- und Texterklärung von Coroutinen in GoLang

尚
nach vorne
2019-11-28 14:14:583902Durchsuche

Detaillierte Grafik- und Texterklärung von Coroutinen in GoLang

Coroutine ist eine leichtgewichtige Thread-Implementierung in der Go-Sprache und wird von der Go-Laufzeit verwaltet.

Fügen Sie das Schlüsselwort go vor einem Funktionsaufruf hinzu, und der Aufruf wird gleichzeitig in einer neuen Goroutine ausgeführt. Wenn die aufgerufene Funktion zurückkehrt, wird auch diese Goroutine automatisch beendet. Es ist zu beachten, dass der Rückgabewert verworfen wird, wenn diese Funktion einen Rückgabewert hat.

Sehen Sie sich zunächst das folgende Beispiel an:

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

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

Wenn Sie den obigen Code ausführen, werden Sie feststellen, dass nichts auf dem Bildschirm gedruckt wird und das Programm beendet wird.

Im obigen Beispiel startet die Funktion main() 10 Goroutinen und kehrt dann zurück. Zu diesem Zeitpunkt wird das Programm beendet und die gestartete Goroutine, die Add() ausführt, hat keine Zeit zur Ausführung. Wir möchten, dass die Funktion main() darauf wartet, dass alle Goroutinen beendet werden, bevor sie zurückkehrt. Aber woher wissen wir, dass alle Goroutinen beendet wurden? Dies führt zu dem Problem der Kommunikation zwischen mehreren Goroutinen.

In der Technik gibt es zwei gängigste gleichzeitige Kommunikationsmodelle: Shared Memory und Messages.

Sehen Sie sich das folgende Beispiel an. Nachdem jede Goroutine ausgeführt wurde, wird der Zählerwert um 1 erhöht. Da 10 Goroutinen gleichzeitig ausgeführt werden, führen wir auch eine Sperre ein die Sperrvariable im Code. Verwenden Sie in der Funktion main() eine for-Schleife, um den Zählerwert kontinuierlich zu überprüfen. Wenn der Wert 10 erreicht, bedeutet dies, dass alle Goroutinen ausgeführt wurden. Zu diesem Zeitpunkt kehrt main() zurück und das Programm wird beendet.

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
        }
    }
}

Das obige Beispiel verwendet eine Sperrvariable (eine Art gemeinsam genutzten Speicher), um die Coroutine zu synchronisieren. Tatsächlich verwendet die Go-Sprache hauptsächlich den Nachrichtenmechanismus (Kanal) als Kommunikationsmodell.

Kanal

Der Nachrichtenmechanismus berücksichtigt, dass jede gleichzeitige Einheit eine in sich geschlossene, unabhängige Person ist und ihre eigenen Variablen hat, jedoch in Diese Variablen werden nicht von verschiedenen gleichzeitigen Einheiten gemeinsam genutzt. Jede gleichzeitige Einheit verfügt nur über einen Ein- und Ausgang, nämlich Nachrichten.

Kanal ist die Kommunikationsmethode zwischen Goroutinen, die von der Go-Sprache auf Sprachebene bereitgestellt wird. Wir können Kanäle verwenden, um Nachrichten zwischen mehreren Goroutinen weiterzuleiten. Der Kanal ist eine prozessinterne Kommunikationsmethode, sodass der Prozess der Übergabe von Objekten über Kanäle mit dem Parameterübergabeverhalten beim Aufrufen von Funktionen übereinstimmt. Beispielsweise können auch Zeiger übergeben werden.
Kanäle sind typbezogen. Ein Kanal kann nur einen Werttyp übergeben. Dieser Typ muss bei der Kanaldeklaration angegeben werden. Die Deklarationsform von

channel lautet:

var chanName chan ElementType

Deklarieren Sie beispielsweise einen Kanal mit dem int-Typ:

var ch chan int

Verwenden Sie die integrierte Funktion make(), um Definieren Sie einen Kanal:

ch := make(chan int)

Bei der Kanalnutzung sind Schreiben und Lesen am häufigsten:

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

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

Standardmäßig sind Kanalempfang und -sende blockiert, es sei denn, das andere Ende ist betriebsbereit.

Wir können auch einen gepufferten Kanal erstellen:

c := make(chan int, 1024)

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

Zu diesem Zeitpunkt erstellen Sie einen Kanal vom Typ int mit einer Größe von 1024. Auch wenn kein Leser vorhanden ist, kann der Autor immer zu ihm wechseln Das Schreiben auf den Kanal wird erst blockiert, wenn der Puffer gefüllt ist.

Sie können den Kanal schließen, der nicht mehr verwendet wird:

close(ch)

Der Kanal sollte beim Produzenten geschlossen werden. Wenn er beim Verbraucher geschlossen wird, kann dies leicht zu Panik führen 🎜>

Bei einer A-Empfangsoperation (Schreiben Sie nun das obige Beispiel mit Kanälen um:

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
    }
}

In diesem Beispiel wird ein Array mit 10 Kanälen definiert und jeder Kanal im Array wird 10 verschiedenen Goroutinen zugewiesen. Nach Abschluss jeder Goroutine werden Daten in die Goroutine geschrieben. Diese Operation blockiert, bis der Kanal gelesen wird.

Nachdem alle Goroutinen gestartet wurden, werden nacheinander Daten von 10 Kanälen gelesen. Dieser Vorgang wird ebenfalls blockiert, bevor der entsprechende Kanal Daten schreibt. Auf diese Weise wird der Kanal zur Implementierung einer sperrenähnlichen Funktion verwendet und stellt sicher, dass main() erst zurückkehrt, nachdem alle Goroutinen abgeschlossen sind.

Wenn wir außerdem eine Kanalvariable an eine Funktion übergeben, können wir die Operationen auf diesem Kanal in der Funktion einschränken, indem wir ihn als Einweg-Kanalvariable angeben.

Deklaration von Einwegkanalvariablen:

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

Sie können einen Kanal durch Typkonvertierung in einen Einwegkanal umwandeln:

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

Die Rolle des Einwegkanals ist etwas Ähnlich wie in C++ wird das const-Schlüsselwort in verwendet, um dem „Prinzip der geringsten Rechte“ im Code zu folgen.

Zum Beispiel die Verwendung eines unidirektionalen Lesekanals in einer Funktion:

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

Als nativer Typ kann der Kanal selbst auch durch den Kanal geleitet werden, wie zum Beispiel die folgende Streaming-Verarbeitungsstruktur:

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

In UNIX wird die Funktion select() zum Überwachen einer Gruppe von Deskriptoren verwendet. Dieser Mechanismus wird häufig zur Implementierung verwendet Sockets-Serverprogramm mit hoher Parallelität. Die Go-Sprache unterstützt direkt das Schlüsselwort select auf Sprachebene, das zur Behandlung asynchroner E/A-Probleme verwendet wird. Die allgemeine Struktur ist wie folgt:

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

    default:
    // 默认分支
}

select blockiert standardmäßig und wird nur ausgeführt, wenn dies der Fall ist Senden oder Empfangen im überwachten Kanal. Wenn mehrere Kanäle bereit sind, wählt „select“ zufällig einen zur Ausführung aus.

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的函数,

Detaillierte Grafik- und Texterklärung von Coroutinen in GoLang

调度

Go调度的几个概念:

M:内核线程;

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

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

Detaillierte Grafik- und Texterklärung von Coroutinen in 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如果发生阻塞等事件会进行阻塞,如下图:

Detaillierte Grafik- und Texterklärung von Coroutinen in GoLang

G发生上下文切换条件:

系统调用;

读写channel;

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

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

Das obige ist der detaillierte Inhalt vonDetaillierte Grafik- und Texterklärung von Coroutinen in GoLang. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:cnblogs.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen