Maison  >  Article  >  développement back-end  >  Explication détaillée de l'utilisation de goroutine en langage go

Explication détaillée de l'utilisation de goroutine en langage go

尚
avant
2019-11-25 14:34:113105parcourir

Explication détaillée de l'utilisation de goroutine en langage go

goroutine in go est une fonctionnalité de go language qui prend en charge la concurrence au niveau de la langue. Lorsque je suis entré en contact avec Go pour la première fois, j'étais extrêmement satisfait de la goroutine de Go. Il était si facile de mettre en œuvre la concurrence que c'en était presque ridicule.

Cependant, au cours du processus de projet, j'ai de plus en plus découvert que la goroutine est une chose dont tout le monde peut facilement abuser. Goroutine est une arme à double tranchant. Voici quelques péchés liés à l'utilisation de goroutine :

1. Le passage du pointeur dans goroutine n'est pas sûr

fun main() {
    request := request.NewRequest() //这里的NewRequest()是传递回一个type Request的指针
    go saveRequestToRedis1(request)
    go saveReuqestToRedis2(request)
     
    select{}
 
}

Code très logique :

La routine principale ouvre une routine pour transmettre la requête à saveRequestToRedis1 et la laisse stocker la requête dans le nœud redis 1.

En même temps, ouvrez une autre routine pour transmettre la requête à saveReuqestToRedis2 et laissez-la stockez la requête. Allez sur le nœud redis 2

Ensuite, la routine principale entre dans la boucle (sans terminer le processus)

Vient maintenant le problème. Les fonctions saveRequestToRedis1 et saveReuqestToRedis2 n'ont pas été écrites par moi, mais par un autre membre de l'équipe Écrit par quelqu'un, je ne sais rien de sa mise en œuvre, et je ne veux pas regarder de plus près la mise en œuvre interne spécifique. Mais d'après le nom de la fonction, je l'ai pris pour acquis et j'ai transmis le pointeur de requête.

En fait, saveRequestToRedis1 et saveRequestToRedis2 sont implémentés comme ceci :

func saveRequestToRedis1(request *Request){
     …
     request.ToUsers = []int{1,2,3} //这里是一个赋值操作,修改了request指向的数据结构
     …
    redis.Save(request)
    return
}

Qu'est-ce qui ne va pas avec ça ? Les deux goroutines saveRequestToRedis1 et saveReuqestToRedis2 modifient la même structure de données partagée, mais comme l'exécution de la routine n'est pas ordonnée, nous ne pouvons pas garantir que le paramètre request.ToUsers et redis.Save() sont une opération atomique, de sorte que le stockage réel de redis se produira. Un bug d’erreur de données.

Eh bien, vous pouvez dire qu'il y a un problème avec l'implémentation de cette fonction saveRequestToRedis, et vous n'avez pas pensé qu'elle serait appelée en utilisant la routine go. Veuillez y réfléchir à nouveau. Il n'y a aucun problème avec l'implémentation spécifique de saveRequestToRedis. Elle ne devrait pas tenir compte de la façon dont la couche supérieure l'utilise.

Autrement dit, il y a un problème avec l'utilisation de ma goroutine. Lorsque la routine principale a ouvert une routine, elle n'a pas confirmé si un code dans la routine avait modifié les données de la routine principale. Oui, la routine principale doit tenir compte de cette situation.

Lorsque la goroutine principale active la routine go, elle doit lire chaque ligne de code dans la sous-routine pour déterminer si les données partagées ont été modifiées ? ? À quel point cela ralentit la vitesse de développement dans le processus de développement du projet lui-même !

Le langage Go utilise la goroutine pour réduire la pression du développement simultané, mais il n'a jamais pensé que cela augmenterait la pression du développement d'un autre côté.

Tout ce qui a été dit ci-dessus est pour tirer une conclusion :

Le dépassement du pointeur Gorotine est dangereux ! !

Si l'exemple précédent n'est pas assez subtil, voici un autre exemple :

fun (this *Request)SaveRedis() {
    redis1 := redis.NewRedisAddr("xxxxxx")
    redis2 := redis.NewRedisAddr("xxxxxx")
    go this.saveRequestToRedis(redis1)
    go this.saveRequestToRedis(redis2)
     
    select{}
}

Peu de gens se demanderont s'il y a un problème avec l'objet pointé par ce pointeur. passé à la routine Il faut dire que c'est très caché.

2. Goroutine augmente le facteur de risque de la fonction

Cela découle en fait du point ci-dessus. Comme mentionné ci-dessus, il n'est pas sûr de transmettre un pointeur vers une fonction Go. Alors réfléchissez-y sous un autre angle, comment pouvez-vous garantir que la fonction que vous souhaitez appeler ne sera pas utilisée dans l'implémentation de la fonction ? Si vous ne regardez pas l'implémentation spécifique à l'intérieur du corps de la fonction, il n'y a aucun moyen de la déterminer.

Par exemple, si nous modifions légèrement l'exemple typique ci-dessus

func main() {
    request := request.NewRequest()
    saveRequestToRedis1(request)
    saveRequestToRedis2(request)
    select{}
}

Maintenant que nous n'utilisons pas la simultanéité, ce problème ne se produira certainement pas, n'est-ce pas ? J'ai couru dans la fonction et j'ai été abasourdi :

func saveReqeustToRedis1(request *Request) {
           …
            go func() {
          …
          request.ToUsers = []{1,2,3}
         ….
         redis.Save(request)
    }
}

a créé une goroutine et modifié l'objet pointé par le pointeur de requête. Une erreur s'est produite ici. Eh bien, si vous ne regardez pas l'implémentation spécifique à l'intérieur de la fonction lors de l'appel de la fonction, ce problème ne peut être évité.

Donc, du pire point de vue, chaque appel de fonction est théoriquement dangereux ! Imaginez, si cette fonction d'appel n'était pas écrite par quelqu'un de ma propre équipe de développement, mais utilisait du code open source tiers sur Internet... Je ne peux vraiment pas imaginer combien de temps il faudrait pour trouver ce bug.

3. Piège à abus Goroutine

Regardez cet exemple :

func main() {
    go saveRequestToRedises(request)
}
 
func saveRequestToRedieses(request *Request) {
    for _, redis := range Redises {
        go redis.saveRequestToRedis(request)
    }
}
 
func saveRequestToRedis(request *Request) {
            ….
            go func() {
                     request.ToUsers = []{1,2,3}
                        …
                        redis.Save(request)
            }
 
}

C'est incroyable, go est partout, semblait-il. surgir de nulle part en un clin d’œil. C'est un abus de go. On voit go partout, mais ce n'est pas très clair. Où doit-on utiliser go ? Pourquoi utiliser Go ? La goroutine améliorera-t-elle vraiment l’efficacité ?

La concurrence en langage C est beaucoup plus compliquée et lourde que la concurrence en langage Go, nous allons donc réfléchir profondément avant de l'utiliser et considérer les avantages et les inconvénients de l'utilisation de la concurrence.

Comment y faire face

Voici quelques façons de gérer ces problèmes :

1 . Au démarrage Lors de l'utilisation d'une goroutine, si une fonction doit passer un pointeur, mais que le niveau de fonction est très profond et que la sécurité ne peut pas être garantie, passez le pointeur vers un clone de l'objet au lieu de passer le pointeur directement

.
fun main() {
    request := request.NewRequest()
    go saveRequestToRedis1(request.Clone())
    go saveReuqestToRedis2(request.Clone())
     
    select{}
 
}

La fonction Clone doit être écrite séparément. Vous pouvez simplement suivre cette méthode une fois la structure définie. Par exemple :

func (this *Request)Clone(){
    newRequest := NewRequst()
    newRequest.ToUsers = make([]int, len(this.ToUsers))
    copy(newRequest.ToUsers, this.ToUsers)
 
}

其实从效率角度考虑这样确实会产生不必要的Clone的操作,耗费一定内存和CPU。但是在我看来,首先,为了安全性,这个尝试是值得的。

其次,如果项目对效率确实有很高的要求,那么你不妨在开发阶段遵照这个原则使用clone,然后在项目优化阶段,作为一种优化手段,将不必要的Clone操作去掉。这样就能在保证安全的前提下做到最好的优化。

2、什么时候使用go的问题

有两种思维逻辑会想到使用goroutine:

1 业务逻辑需要并发

比如一个服务器,接收请求,阻塞式的方法是一个请求处理完成后,才开始第二个请求的处理。其实在设计的时候我们一定不会这么做,我们会在一开始就已经想到使用并发来处理这个场景,每个请求启动一个goroutine为它服务,这样就达到了并行的效果。这种goroutine直接按照思维的逻辑来使用goroutine

2 性能优化需要并发

一个场景是这样:需要给一批用户发送消息,正常逻辑会使用

for _, user := range users {
    sendMessage(user)
 
}

但是在考虑到性能问题的时候,我们就不会这样做,如果users的个数很大,比如有1000万个用户?我们就没必要将1000万个用户放在一个routine中运行处理,考虑将1000万用户分成1000份,每份开一个goroutine,一个goroutine分发1万个用户,这样在效率上会提升很多。这种是性能优化上对goroutine的需求

按照项目开发的流程角度来看。在项目开发阶段,第一种思路的代码实现会直接影响到后续的开发实现,因此在项目开发阶段应该马上实现。

但是第二种,项目中是由很多小角落是可以使用goroutine进行优化的,但是如果在开发阶段对每个优化策略都考虑到,那一定会直接打乱你的开发思路,会让你的开发周期延长,而且很容易埋下潜在的不安全代码。

因此第二种情况在开发阶段绝不应该直接使用goroutine,而该在项目优化阶段以优化的思路对项目进行重构。

推荐:golang开发栏目

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