Démarrez rapidement avec la programmation simultanée GO

Cet article part du combat réel et présente la pratique de la concurrence du langage GO du point de vue de la compétition entre les threads et de la collaboration entre les threads . Enfin, il se concentre sur l'utilisation des canaux dans GO, qui est l'essence de la concurrence du langage GO. commençons!

1 Concurrence entre les fils

Lorsque plusieurs threads effectuent des opérations concurrentes sur des données partagées dans le même langage de haut niveau , une incohérence des données en résultera. L'utilisation de « verrous » peut éviter d'utiliser simultanément des données partagées et peut résoudre des problèmes de concurrence. Mais l'utilisation incorrecte de « verrous », tels que les blocages , est un nouveau problème. Dans le langage GO, l'existence de « verrous » est prise en charge, comprenant principalement les verrous mutex et les verrous en lecture-écriture. GO prend également en charge d'autres méthodes pour éviter la concurrence. Les plus courantes incluent les opérations atomiques, les verrous MAP, etc.

verrouillage mutex

goroutineLes verrous Mutex garantissent qu'une seule personne peut accéder aux données partagées en même temps . Le langage Go utilise syncdes packages pour implémenter des verrous mutex. Un exemple d'utilisation classique est le suivant :

package main

import (
	"fmt"
	"sync"
	"time"
)

type test struct {
	x int
	mutex sync.Mutex
}

func (t *test) add(){
	//对t.x++操作进行加锁,避免竞争。
	//如果不加锁,会出现错误的结果
	t.mutex.Lock()
	t.x++
	t.mutex.Unlock()
}

func (t *test) get()int{
	return t.x
}

func main() {
	t:=test{x: 0}
	for i := 0; i < 1000; i++ {
	//并发的运行添加操作
		go t.add()
	}
	//这里之所以加休止,是因为main函数也是一个特殊的go程。所以很有可能前面开启的go程还没运行完,main程已经选择输出了,也会出现错误的结果。
	//此处也可以使用WaitGroup方法,后面会介绍。
	time.Sleep(100)
	fmt.Println(t.get())
}

verrouillage en lecture-écriture

Dans le cas de plus de lecture et moins d'écriture, les verrous en lecture-écriture peuvent être utilisés en premier . L'utilisation du verrou en lecture-écriture et du verrou mutex est très similaire. Le verrou en lecture permet aux ressources partagées d'être lues arbitrairement mais ne peut pas être écrite. Le verrou en écriture est un verrou mutex. La bibliothèque de synchronisation est utilisée dans le langage GO pour fournir des verrous en lecture et en écriture. Un exemple d'utilisation classique est le suivant :

package main

import (
	"fmt"
	"sync"
	"time"
)

// 声明读写锁
var rwlock sync.RWMutex
var x int

// 写数据
func write() {
	rwlock.Lock()
	x += 1
	rwlock.Unlock()
}

func read(i int) {
	rwlock.RLock()
	fmt.Println(x)
	rwlock.RUnlock()
}

func main() {
	go write()
	for i := 0; i < 1000; i++ {
		go read(i)
	}
	time.Sleep(100)
}

verrouillage de la carte

La carte dans GO prend en charge la lecture simultanée, mais elle ne prend pas en charge l'écriture simultanée. Toutefois, vous pouvez utiliser syncla carte qui prend en charge la simultanéité pour obtenir la simultanéité. L'exemple de code est le suivant :

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	var m = sync.Map{}

	// 并发写
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			m.Store(i,i*i)
			}(i)
	}
	wg.Wait()
	fmt.Println(m.Load(1))
}

Opérations atomiques

Vous pouvez également utiliser sync/atomicdes bibliothèques pour effectuer des opérations atomiques afin de garantir la sécurité des threads. Les opérations atomiques peuvent être effectuées en mode utilisateur et leurs performances sont supérieures à celles du verrouillage.Il existe principalement les méthodes atomiques suivantes :

//加减方法
AddXxx()
//读取方法
LoadXxx()
//写入方法
StoreXxx()
//交换方法
SwapXxx()

2 Coopération entre les threads

Il existe non seulement une relation de compétition entre les fils, mais aussi une relation de coopération. Par exemple, le modèle consommateur/producteur nécessite une coopération entre différents threads. La méthode de réalisation de la collaboration dans le langage GO peut non seulement utiliser des méthodes traditionnelles telles que les variables de condition, mais également réaliser une collaboration via un mécanisme de canal unique. Ensuite, WaitGroup, les variables de condition et les canaux seront introduits dans l'ordre.

Groupe d'attente

WaitGroup peut être utilisé dans GO pour réaliser la synchronisation entre plusieurs processus GO. Au sein sync.WaitGroupd'un type, chaque valeur sync.WaitGroup maintient en interne un décompte avec une valeur par défaut initiale de zéro. Ce compteur peut être modifié en appelant les méthodes suivantes.

nom de la méthode Fonction
Ajouter (x int) Compteur de groupe d'attente +1
Fait() Compteur de groupe d'attente -1
Attendez() Lorsque le compteur du groupe d'attente n'est pas égal à 0, bloquez jusqu'à ce qu'il devienne 0.

Il est généralement utilisé comme fonction principale du processus GO pour attendre la fin des autres processus GO. Il est simplement utilisé comme suit :

package main

import (
	"fmt"
	"sync"
)
//声明一个计数器
var wg sync.WaitGroup

type test struct {
	x int
	mutex sync.Mutex
}

func (t *test) add(){
	//运行完毕 计数器减一
	defer wg.Done()
	//对t.x++操作进行加锁,避免竞争。
	//如果不加锁,会出现错误的结果
	t.mutex.Lock()
	t.x++
	t.mutex.Unlock()
}

func (t *test) get()int{
	return t.x
}

func main() {
	t:=test{x: 0}
	for i := 0; i < 1000; i++ {
	   //运行一个新GO程,计数器加一
		wg.Add(1)
	   //并发的运行添加操作
		go t.add()
	}
	//直到计数器为0,不然会一直阻塞
	wg.Wait()
	fmt.Println(t.get())
}

variable de condition

Les variables de condition sont généralement utilisées avec les verrous mutex pour implémenter le modèle d'attente/réveil et constituent une partie importante de la collaboration. Il peut bloquer un thread jusqu'à ce qu'il reçoive un signal de réveil externe, puis l'exécuter à nouveau. Les variables de condition dans GO sont basées sur des verrous et syncimplémentées dans la bibliothèque. Voici un exemple simple de variables de condition dans GO.

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var cond *sync.Cond

func test(x int) {
	defer wg.Done()
	cond.L.Lock() // 获取锁
	cond.Wait()   // 等待通知 暂时阻塞
	fmt.Println(x)
	time.Sleep(time.Second * 1)
	cond.L.Unlock()
}

func main() {
	cond = sync.NewCond(&sync.Mutex{})
	fmt.Println("start all")
	for i := 0; i < 40; i++ {
		wg.Add(1)
		go test(i)
	}
	time.Sleep(time.Second * 3)
	fmt.Println("one")
	cond.Signal() // 下发一个通知给已经阻塞的goroutine
	time.Sleep(time.Second * 3)
	fmt.Println("one")
	cond.Signal() // 3秒之后 下发一个通知给已经阻塞的goroutine
	time.Sleep(time.Second * 3)
	fmt.Println("broadcast")
	cond.Broadcast() //3秒之后 下发广播给所有等待的goroutine
	wg.Wait()
}

3 canaux

Ne communiquez pas via la mémoire partagée, partagez la mémoire via la communication

Les verrous et les opérations atomiques présentés ci-dessus sont pris en charge par la plupart des langages de programmation. Ensuite, le canal de composant principal de concurrence (pipeline) du langage GO sera présenté.

Les chaînes et les Goroutines se complètent. En tant que concurrence unique de GO, les processus GO sont des coroutines en mode utilisateur. Ils sont très légers et peuvent facilement exécuter des milliers de processus GO sur un ordinateur avec des performances moyennes. La fonction principale de Channel est de servir de canal de communication pour fournir un pont de communication entre les processus GO. Il a pour fonction de recevoir et d'envoyer des données , semblable à une file d'attente globale simultanée et sécurisée. Les concepteurs du langage GO mettent l'accent sur le fait de ne pas communiquer via la mémoire partagée, mais de partager la mémoire via la communication . C'est l'idée de conception du langage GO. L'utilisation des canaux sera présentée ensuite.

Définir et créer un canal

La définition du canal est très similaire à celle de la carte. Tout d'abord, la structure des données du canal peut être définie par vous-même. Par exemple, le code suivant définit un canal qui peut stocker le type Int.

var ch chan int

Faites attention au code ci-dessus et faites attention à quelques points :

  • Après avoir défini ch comme chan de type int, ch est toujours une valeur nulle et ne peut pas être utilisé directement ! Tout comme pour l'utilisation de map, vous devez utiliser .
  • Non seulement vous pouvez définir des chan de type int, mais vous pouvez également définir des chan d'autres types.

La création d'un canal est également très similaire à celle d'une carte. Elle peut être créée à l'aide du code suivant :

ch=make(chan int,10)

Avis:

  • Le type spécifié lors de la création doit être cohérent avec le type lors de la définition. Chan ne peut être utilisé normalement qu'après sa création.
  • Lors de la création, vous pouvez spécifier la taille du canal , dans l'exemple ci-dessus elle est de 10. Lorsque la taille est 0, le canal créé est un canal non tamponné , et lorsqu'elle est supérieure à 0, le canal créé est tamponné .

Bien entendu, vous pouvez également conjuguer définition et création :

ch:=make(chan int,10)

Opérations de base des chaînes

Le canal a pour fonction d'envoyer et de recevoir des données. Les méthodes d'écriture sont les suivantes :

x := 0
//创建通道ch
ch:= make(chan int,1)
// 向ch发送数据
ch <- 1
// 从ch接收数据
x = <- ch

Avis:

  • Chaque canal a une taille de tampon. Lorsque le tampon est plein, l'envoi sera bloqué, et lorsque le tampon est vide, la réception sera bloquée.
  • Un seul envoi et une seule réception d'un canal sans tampon seront bloqués et l'opération ne pourra continuer que lorsque les paires d'envoi et de réception existeront.

Selon la classification opérationnelle des canaux, les pipelines peuvent également être divisés en canaux ordinaires et canaux unidirectionnels. Les définitions sont les suivantes :

//只能向通道发送数据
var ch chan-< int
//只能从通道接受数据
var ch <-chan int  
  • De manière générale, lors du passage d'un canal en tant que valeur de fonction à une fonction, un canal unidirectionnel est utilisé, ce qui limite les opérations de la fonction sur le canal.

Ensuite, écrivez un exemple de deux personnes jouant au tennis de table pour démontrer l'utilisation du canal :

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wg sync.WaitGroup

func A(ch chan int) {
	defer wg.Done()
	for {
		time.Sleep(time.Second)
		//得到击球机会
		num := <-ch
		//如果是有效的回球
		if num >= 1 {
			//产生随机数回球,如果数小于300,代表没打到球,输掉比赛。
			temp := rand.Int63() % 1000
			fmt.Println("A击出第", num, "球,得到", temp, "分!")
			if temp < 300 {
				fmt.Println("A输掉比赛!")
				ch <- 0
				break
			}
			ch <- num + 1
		} else {
			//如果是无效的球,退出循环
			break
		}
	}
	fmt.Println("A结束比赛~")
}
//B和A类似
func B(ch chan int) {
	defer wg.Done()
	for {
		time.Sleep(time.Second)
		//得到击球机会
		num := <-ch
		if num >= 1 {
			temp := rand.Int63() % 1000
			fmt.Println("B击出第", num, "球,得到", temp, "分!")
			if temp < 300 {
				fmt.Println("B输掉比赛!")
				ch <- 0
				break
			}
			ch <- num + 1
		} else {
			break
		}
	}
	fmt.Println("B结束比赛~")
}

func main() {
	ch := make(chan int)
	wg.Add(2)
	//启动双方
	go A(ch)
	go B(ch)
	//发球
	ch <- 1
	//等待比赛结束
	wg.Wait()
}

sélectionner

Après avoir appris les opérations de base de chan, vous devez encore apprendre une chose très importante : sélectionner. La syntaxe de select et switch est très similaire, mais les méthodes d'utilisation sont complètement différentes. Il peut être compris comme un commutateur dédié à la communication , et dans chaque cas il doit s'agir d'une opération de communication par canal (réception ou envoi).

Remarque : L' selectinstruction du langage Go est empruntée à la select()fonction Unix. Sous Unix, vous pouvez select()surveiller une série de descripteurs de fichiers en appelant la fonction. Une fois qu'une action d'E/S se produit sur l'un des descripteurs de fichiers, l' select()appel sera renvoyé (c'est le cas en langage C (fait), et plus tard, ce mécanisme a également été utilisé pour implémenter des programmes serveur Socket à haute concurrence. Le langage Go prend directement en charge les mots-clés au niveau du langage selectpour gérer les problèmes de communication IO asynchrone entre les canaux dans la programmation simultanée. ——"Huben GO (le langage C au 21e siècle)"

Voici la syntaxe de base :

select {
    case <-ch:
        // 如果从 ch 信道成功接收数据,则执行该分支代码
    case ch1 <- 1:
        // 如果成功向 ch1 信道成功发送数据,则执行该分支代码
    // 你可以定义任意数量的 case 
    //default为可选项
    default : 
     //如果没有case可运行,则运行此分支
}

Faites attention à quelques points :

  • Chaque cas doit être suivi d'une opération de communication sur chan .

  • Toutes les expressions suivant le cas ne seront pas exécutées . S'il existe plusieurs cas exécutables , select exécutera de manière aléatoire un cas exécutable et exécutera le code de branche en dessous.

  • S'il n'y a aucun cas à exécuter, la branche par défaut sera exécutée. S'il n'y a pas de branche par défaut, elle se bloquera jusqu'à ce qu'il y ait un cas à exécuter.

sélection et minuterie

Select est souvent utilisé avec des minuteries pour effectuer certaines opérations de manière régulière. Il est généralement utilisé pour surveiller si un service est terminé dans un délai spécifié. Voici un code simple que vous pouvez essayer de modifier :

package main

import (
	"fmt"
	"time"
)

func A(ch chan int) {
	time.Sleep(time.Second * 2)
	ch <- 1
}

func main() {
	ch := make(chan int)
	go A(ch)
	select {
	//接受来自ch的消息
	case <-ch:
		fmt.Println("接收到消息!")
		//定时器设置为1s,超过1s后case被激活
	case <-time.After(1 * time.Second):
		fmt.Println("没接收到消息!")
	}
}

Résumer

Dans cet article, nous présentons principalement la syntaxe de base de la concurrence du langage GO et donnons quelques petites démos. La colonne linguistique GO de l'auteur contient également quelques exemples pratiques de versions avancées. Les amis intéressés peuvent accéder à la page d'accueil et rechercher « concurrence GO » pour voir.

Je suppose que tu aimes

Origine blog.csdn.net/doreen211/article/details/125066246
conseillé
Classement