Le canal de Golang est un élément clé de son modèle de concurrence CSP et un pont de communication entre les Goroutines. Channel est fréquemment utilisé dans Golang et il est crucial d'avoir une compréhension approfondie de ses principes de mise en œuvre internes. Cet article analysera l'implémentation sous-jacente de Channel basée sur le code source Go 1.13.
Avant d'analyser formellement la mise en œuvre de Channel, passons en revue son utilisation de base :
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
Ce code montre deux opérations de base de Channel :
c <- 1
x := <-c
Le canal est divisé en canal tamponné et canal non tamponné. Le code ci-dessus utilise un canal non tamponné. Dans un canal non tamponné, si aucun autre Goroutine ne reçoit actuellement de données, l'expéditeur bloquera l'instruction d'envoi.
Vous pouvez spécifier la taille du tampon lors de l'initialisation du canal. Par exemple, make(chan int, 2)
spécifie que la taille du tampon est de 2. Avant que le tampon ne soit plein, l'expéditeur peut envoyer des données sans blocage sans attendre que le destinataire soit prêt. Mais si le tampon est plein, l'expéditeur bloquera toujours.
Avant de plonger dans le code source de Channel, vous devez trouver l'emplacement d'implémentation spécifique de Channel dans Golang. Lors de l'utilisation de Channel, les fonctions sous-jacentes telles que runtime.makechan
, runtime.chansend
et runtime.chanrecv
sont en fait appelées.
Vous pouvez utiliser la commande go tool compile -N -l -S hello.go
pour convertir le code en instructions d'assemblage, ou utiliser l'outil en ligne Compiler Explorer (par exemple : go.godbolt.org/z/3xw5Cj). En analysant la notice de montage, on peut retrouver :
make(chan int)
correspond à la fonction runtime.makechan
. c <- 1
correspond à la fonction runtime.chansend
. x := <-c
correspond à la fonction runtime.chanrecv
. L'implémentation de ces fonctions se trouve dans le fichier runtime/chan.go
du code source de Go.
make(chan int)
sera converti en fonction runtime.makechan
par le compilateur, et sa signature de fonction est la suivante :
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
Parmi eux, t *chantype
est le type d'élément Channel, size int
est la taille du tampon spécifiée par l'utilisateur (0 si non spécifiée) et la valeur de retour est *hchan
. hchan
est la structure de mise en œuvre interne de Channel in Golang, définie comme suit :
<code class="language-go">type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }</code>
Les attributs de hchan
sont grossièrement divisés en trois catégories :
buf
, dataqsiz
, qcount
, etc. Lorsque la taille du tampon du canal n'est pas 0, le tampon est utilisé pour stocker les données à recevoir et est implémenté à l'aide d'un tampon en anneau. recvq
contient Goroutine en attente de réception de données, sendq
contient Goroutine en attente d'envoi de données. waitq
Mise en œuvre à l'aide d'une liste doublement chaînée. lock
, elemtype
, closed
, etc. makechan
effectue principalement des contrôles de légalité et l'allocation de mémoire d'attributs tels que les tampons et hchan
, qui ne seront pas abordés en profondeur ici.
Sur la base d'une simple analyse de l'attribut hchan
, on peut voir qu'il existe deux composants importants : le tampon et la file d'attente. Tous les comportements et implémentations de hchan
tournent autour de ces deux composants.
Les processus d'envoi et de réception de Channel sont très similaires. Analysez d'abord le processus d'envoi de Channel (par exemple c <- 1
).
essaie d'envoyer des données au canal, si la file d'attente recvq
n'est pas vide, une Goroutine en attente de réception de données sera retirée de l'en-tête recvq
et les données seront envoyées directement à la Goroutine. Le code est le suivant :
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
recvq
Contient Goroutine en attente de réception de données. Lorsqu'une Goroutine utilise une opération de réception (telle que x := <-c
), si sendq
n'est pas vide à ce moment, une Goroutine sera extraite de sendq
et les données lui seront envoyées.
Si recvq
est vide, cela signifie qu'il n'y a pas de Goroutine en attente de recevoir des données à ce moment-là, et la chaîne essaiera de mettre les données dans le tampon :
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
La fonction de ce code est très simple, c'est de mettre des données dans le tampon. Ce processus implique le fonctionnement d'un tampon en anneau, dataqsiz
représente la taille du tampon spécifiée par l'utilisateur (la valeur par défaut est 0 si elle n'est pas spécifiée).
Si un canal non tamponné est utilisé ou si le tampon est plein (c.qcount == c.dataqsiz
), les données à envoyer et la Goroutine actuelle seront regroupées dans un objet sudog
, placé dans sendq
, et le courant Goroutine sera configuré pour attendre Statut :
<code class="language-go">type hchan struct { qcount uint // 缓冲区中已放入元素的数量 dataqsiz uint // 用户构造Channel时指定的缓冲区大小 buf unsafe.Pointer // 缓冲区 elemsize uint16 // 缓冲区中每个元素的大小 closed uint32 // Channel是否关闭,==0表示未关闭 elemtype *_type // Channel元素的类型信息 sendx uint // 缓冲区中发送元素的索引位置(发送索引) recvx uint // 缓冲区中接收元素的索引位置(接收索引) recvq waitq // 等待接收的Goroutine列表 sendq waitq // 等待发送的Goroutine列表 lock mutex }</code>
goparkunlock
déverrouillera le mutex d'entrée et suspendra le Goroutine actuel, le mettant dans un état d'attente. gopark
et goready
apparaissent par paires et sont des opérations réciproques.
Du point de vue de l'utilisateur, après avoir appelé gopark
, l'instruction de code pour l'envoi de données sera bloquée.
Le processus de réception de Channel est fondamentalement similaire au processus d'envoi, je n'entrerai donc pas dans les détails ici. Les opérations liées au tampon impliquées dans le processus de réception seront décrites en détail ultérieurement.
Il convient de noter que l'ensemble du processus d'envoi et de réception de Channel est verrouillé à l'aide de runtime.mutex
. runtime.mutex
est un verrou léger couramment utilisé dans le code source lié à l'exécution. L'ensemble du processus n'est pas la solution sans verrou la plus efficace. Il y a un problème concernant la chaîne sans verrouillage dans Golang : go/issues#8899.
Channel utilise un tampon en anneau pour mettre en cache les données écrites. Les tampons en anneau présentent de nombreux avantages et sont idéaux pour implémenter des files d'attente FIFO de longueur fixe.
L'implémentation du ring buffer dans Channel est la suivante :
Il y a deux variables liées au tampon danshchan
: recvx
et sendx
. sendx
représente un index inscriptible dans le tampon, et recvx
représente un index lisible dans le tampon. Les éléments entre recvx
et sendx
représentent les données qui ont été normalement placées dans le tampon.
Vous pouvez directement utiliser buf[recvx]
pour lire le premier élément de la file d'attente, et utiliser buf[sendx] = x
pour mettre l'élément en fin de file d'attente.
Lorsque le tampon n'est pas plein, l'opération de mise des données dans le tampon est la suivante :
<code class="language-go">package main import "fmt" func main() { c := make(chan int) go func() { c <- 1 // 发送操作 }() x := <-c // 接收操作 fmt.Println(x) }</code>
chanbuf(c, c.sendx)
équivaut à c.buf[c.sendx]
. Le processus ci-dessus est très simple, copiez simplement les données vers l'emplacement tampon sendx
.
Ensuite, passez sendx
à la position suivante. Si sendx
atteint la dernière position, il est mis à 0, ce qui est une approche typique de bout en bout.
Lorsque le tampon n'est pas plein, sendq
doit également être vide (car si le tampon n'est pas plein, la Goroutine envoyant les données ne sera pas mise en file d'attente, mais mettra directement les données dans le tampon). À l'heure actuelle, la logique de lecture du canal chanrecv
est relativement simple. Les données peuvent être lues directement à partir du tampon. C'est également un processus de déplacement de recvx
, qui est fondamentalement le même que l'écriture du tampon ci-dessus.
Quand il y a une Goroutine en attente dans sendq
, le tampon doit être plein à ce moment. A l'heure actuelle, la logique de lecture de Channel est la suivante :
<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
ep
est l'adresse correspondant à la variable qui reçoit les données (par exemple, dans x := <-c
, ep
est l'adresse de x
). sg
représente le premier sendq
extrait de sudog
. Dans le code :
typedmemmove(c.elemtype, ep, qp)
signifie copier l'élément actuellement lisible dans le tampon à l'adresse de la variable réceptrice. typedmemmove(c.elemtype, qp, sg.elem)
signifie copier les données en attente d'envoi par Goroutine dans sendq
dans le tampon. Parce que recv
est exécuté plus tard, cela équivaut à placer les données dans sendq
à la fin de la file d'attente. En termes simples, ici Channel copie les premières données du tampon dans la variable de réception correspondante, et copie en même temps les éléments de sendq
à la fin de la file d'attente, implémentant ainsi FIFO (premier entré, premier sorti) .
Channel est l'une des fonctionnalités les plus couramment utilisées dans Golang. Comprendre son code source vous aidera à mieux utiliser et comprendre Channel. Dans le même temps, ne soyez pas trop superstitieux et comptez sur les performances de Channel. La conception actuelle de Channel a encore beaucoup de place pour l'optimisation.
Suggestions d'optimisation :
Enfin, je recommande une plateforme très adaptée au déploiement des services Go : Leapcell
Veuillez consulter la documentation pour plus d'informations !
Twitter de Leapcell : https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd
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!