Maison  >  Article  >  développement back-end  >  Explication graphique et textuelle détaillée des coroutines dans GoLang

Explication graphique et textuelle détaillée des coroutines dans GoLang

尚
avant
2019-11-28 14:14:583921parcourir

Explication graphique et textuelle détaillée des coroutines dans GoLang

Coroutine est une implémentation de thread légère dans le langage Go et est gérée par le runtime Go.

Ajoutez le mot-clé go avant un appel de fonction, et l'appel sera exécuté simultanément dans une nouvelle goroutine. Lorsque la fonction appelée revient, cette goroutine se termine également automatiquement. Il convient de noter que si cette fonction a une valeur de retour, la valeur de retour sera ignorée.

Regardez d'abord l'exemple suivant :

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

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

Lorsque vous exécutez le code ci-dessus, vous constaterez que rien n'est imprimé à l'écran et le programme se termine.

Pour l'exemple ci-dessus, la fonction main() démarre 10 goroutines puis revient. À ce moment, le programme se termine et la goroutine démarrée qui exécute Add() n'a pas le temps de s'exécuter. Nous voulons que la fonction main() attende que toutes les goroutines se terminent avant de revenir, mais comment savoir que toutes les goroutines sont terminées ? Cela conduit au problème de la communication entre plusieurs goroutines.

En ingénierie, il existe deux modèles de communication simultanés les plus courants : la mémoire partagée et les messages.

Regardez l'exemple suivant. 10 goroutines partagent le compteur variable. Après que chaque goroutine soit exécutée, la valeur du compteur est augmentée de 1. Étant donné que 10 goroutines sont exécutées simultanément, nous introduisons également un verrou. la variable de verrouillage dans le code. Dans la fonction main(), utilisez une boucle for pour vérifier en permanence la valeur du compteur. Lorsque sa valeur atteint 10, cela signifie que toutes les goroutines ont été exécutées. À ce moment, main() revient et le programme se termine.

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

L'exemple ci-dessus utilise une variable de verrouillage (un type de mémoire partagée) pour synchroniser la coroutine. En fait, le langage Go utilise principalement le mécanisme de message (canal) comme modèle de communication.

canal

Le mécanisme de message considère que chaque unité concurrente est un individu autonome et indépendant et possède ses propres variables, mais dans celles-ci les variables ne sont pas partagées entre différentes unités concurrentes. Chaque unité concurrente n'a qu'une seule entrée et sortie, qui sont des messages.

Le canal est la méthode de communication entre les goroutines fournie par le langage Go au niveau du langage. Nous pouvons utiliser des canaux pour transmettre des messages entre plusieurs goroutines. Le canal est une méthode de communication intra-processus, de sorte que le processus de transmission d'objets via des canaux est cohérent avec le comportement de transmission des paramètres lors de l'appel de fonctions. Par exemple, des pointeurs peuvent également être transmis.
Les canaux sont liés au type. Un canal ne peut transmettre qu'un seul type de valeur. Ce type doit être spécifié lors de la déclaration du canal. Le formulaire de déclaration de

channel est :

var chanName chan ElementType

Par exemple, déclarez un canal en passant le type int :

var ch chan int

Utilisez la fonction intégrée make() pour définir un canal :

ch := make(chan int)

Dans l'utilisation des canaux, les plus courants incluent l'écriture et la lecture :

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

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

Par défaut, la réception et l'envoi du canal sont bloqués sauf si l'autre extrémité est prête.

On peut aussi créer un canal bufferisé :

c := make(chan int, 1024)

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

A ce moment, créez un canal de type int d'une taille de 1024. Même s'il n'y a pas de lecteur, l'écrivain peut toujours aller sur L'écriture sur le canal ne sera pas bloquée tant que le tampon n'est pas rempli.

Vous pouvez fermer la chaîne qui n'est plus utilisée :

close(ch)

La chaîne doit être fermée chez le producteur Si elle est fermée chez le consommateur, cela provoquera facilement la panique

Dans une opération de réception A (Réécrivez maintenant l'exemple ci-dessus en utilisant les canaux :

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

Dans cet exemple, un tableau contenant 10 canaux est défini, et chaque canal du tableau est attribué à 10 goroutines différentes. Une fois chaque goroutine terminée, une donnée est écrite dans la goroutine. Cette opération se bloque jusqu'à ce que le canal soit lu.

Une fois toutes les goroutines démarrées, les données sont lues à partir de 10 canaux en séquence. Cette opération est également bloquée avant que le canal correspondant n'écrive des données. De cette façon, le canal est utilisé pour implémenter une fonction de type verrou et garantit que main() ne revient qu'une fois toutes les goroutines terminées.

De plus, lorsque l'on passe une variable de canal à une fonction, nous pouvons limiter les opérations sur ce canal dans la fonction en le spécifiant comme variable de canal unidirectionnel.

Déclaration des variables de canal unidirectionnel :

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

Vous pouvez convertir un canal en conversion de type unidirectionnelle :

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

Le rôle du canal unidirectionnel est quelque peu similaire à c++ Le mot-clé const in est utilisé pour suivre le « principe du moindre privilège » dans le code.

Par exemple, en utilisant un canal de lecture unidirectionnel dans une fonction :

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

En tant que type natif, le canal lui-même peut également être transmis via le canal, comme la structure de traitement de streaming suivante :

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

Sous UNIX, la fonction select() est utilisée pour surveiller un groupe de descripteurs. Ce mécanisme est souvent utilisé pour implémenter. sockets à haute concurrence. Programme serveur. Le langage Go prend directement en charge le mot-clé select au niveau du langage, qui est utilisé pour traiter les problèmes d'E/S asynchrones. La structure générale est la suivante :

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

    default:
    // 默认分支
}

select bloque par défaut et ne se produira que lorsqu'il y en aura. envoi ou réception dans le canal surveillé Lors de l'exécution, lorsque plusieurs canaux sont prêts, sélectionnez-en un au hasard pour l'exécution.

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

Explication graphique et textuelle détaillée des coroutines dans GoLang

调度

Go调度的几个概念:

M:内核线程;

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

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

Explication graphique et textuelle détaillée des coroutines dans 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如果发生阻塞等事件会进行阻塞,如下图:

Explication graphique et textuelle détaillée des coroutines dans GoLang

G发生上下文切换条件:

系统调用;

读写channel;

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer