Heim  >  Artikel  >  Backend-Entwicklung  >  Detaillierte Erläuterung der Verwendung von Goroutine in der Go-Sprache

Detaillierte Erläuterung der Verwendung von Goroutine in der Go-Sprache

尚
nach vorne
2019-11-25 14:34:113104Durchsuche

Detaillierte Erläuterung der Verwendung von Goroutine in der Go-Sprache

goroutine in go ist eine Funktion von go language, die Parallelität auf Sprachebene unterstützt. Als ich zum ersten Mal mit Go in Kontakt kam, war ich mit Gos Goroutine äußerst zufrieden. Es ist so einfach, Parallelität zu implementieren, dass es fast lächerlich ist.

Während des Projektprozesses stellte ich jedoch zunehmend fest, dass Goroutine eine Sache ist, die leicht von jedem missbraucht werden kann. Goroutine ist ein zweischneidiges Schwert. Hier sind ein paar Sünden bei der Verwendung von Goroutine:

1. Die Zeigerübergabe in Goroutine ist unsicher

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

Sehr logischer Code:

Die Hauptroutine öffnet eine Routine, um die Anfrage an saveRequestToRedis1 weiterzuleiten, und lässt sie die Anfrage im Redis-Knoten 1 speichern.

Öffnen Sie gleichzeitig eine andere Routine, um die Anfrage an saveReuqestToRedis2 zu übergeben, und lassen Sie sie Speichern Sie die Anfrage. Gehen Sie zu Redis-Knoten 2

Dann tritt die Hauptroutine in die Schleife ein (ohne den Prozess zu beenden)

Jetzt kommt das Problem: Die Funktionen saveRequestToRedis1 und saveReuqestToRedis2 wurden nicht von mir geschrieben. Aber von einem anderen Mitglied des Teams. Ich weiß nichts über die Implementierung und möchte nicht näher auf die spezifische interne Implementierung eingehen. Aber dem Funktionsnamen zufolge habe ich ihn als selbstverständlich angesehen und den Anforderungszeiger übergeben.

Tatsächlich werden saveRequestToRedis1 und saveRequestToRedis2 wie folgt implementiert:

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

Was ist daran falsch? Die beiden Goroutinen saveRequestToRedis1 und saveReuqestToRedis2 ändern dieselbe gemeinsame Datenstruktur. Da die Ausführung der Routine jedoch ungeordnet ist, können wir nicht garantieren, dass die Einstellung „request.ToUsers“ und redis.Save() eine atomare Operation sind, sodass die tatsächliche Speicherung von Redis erfolgt Es wird ein Datenfehlerfehler auftreten.

Nun, man kann sagen, dass es ein Problem mit der Implementierung dieser saveRequestToRedis-Funktion gibt und Sie nicht daran gedacht haben, dass sie mit der Go-Routine aufgerufen wird. Bitte denken Sie noch einmal darüber nach. Es gibt kein Problem mit der spezifischen Implementierung von saveRequestToRedis. Es sollte nicht berücksichtigt werden, wie die obere Ebene es verwendet.

Das heißt, es gibt ein Problem bei der Verwendung meiner Goroutine. Als die Hauptroutine eine Routine öffnete, wurde nicht bestätigt, ob ein Code in der Routine die Daten in der Hauptroutine geändert hat. Ja, die Hauptroutine muss diese Situation berücksichtigen.

Wenn die Haupt-Goroutine die Go-Routine aktiviert, muss sie jede Codezeile in der Unterroutine lesen, um festzustellen, ob die gemeinsam genutzten Daten geändert wurden? ? Wie sehr verlangsamt dies die Entwicklungsgeschwindigkeit im eigentlichen Projektentwicklungsprozess!

Die Go-Sprache verwendet Goroutine, um den Druck der gleichzeitigen Entwicklung zu verringern, hätte aber nie gedacht, dass dies andererseits den Entwicklungsdruck erhöhen würde.

Alles, was oben gesagt wurde, soll eine Schlussfolgerung ziehen:

Das Weitergeben des Gorotine-Zeigers ist unsicher! !

Wenn das vorherige Beispiel nicht subtil genug ist, hier ist ein weiteres Beispiel:

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

Nur ​​wenige Leute werden darüber nachdenken, ob es ein Problem mit dem Objekt gibt, auf das dieser Zeiger zeigt an die Routine übergeben Es sollte gesagt werden, dass es sehr versteckt ist.

2. Goroutine erhöht den Risikofaktor der Funktion

Dies ergibt sich tatsächlich aus dem obigen Punkt. Wie oben erwähnt, ist es unsicher, einen Zeiger an eine Go-Funktion zu übergeben. Betrachten Sie es also aus einem anderen Blickwinkel: Wie können Sie garantieren, dass die Funktion, die Sie aufrufen möchten, nicht in der Funktionsimplementierung enthalten ist? Wenn Sie sich die spezifische Implementierung im Funktionskörper nicht ansehen, gibt es keine Möglichkeit, sie zu bestimmen.

Wenn wir zum Beispiel das obige typische Beispiel leicht ändern

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

Da wir jetzt keine Parallelität verwenden, wird dieses Problem definitiv nicht auftreten, oder? Ich bin in die Funktion gejagt und war verblüfft:

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

hat eine Goroutine erstellt und das Objekt geändert, auf das der Anforderungszeiger zeigt. Hier ist ein Fehler aufgetreten. Nun, wenn Sie beim Aufrufen der Funktion nicht auf die spezifische Implementierung innerhalb der Funktion achten, lässt sich dieses Problem nicht vermeiden.

Im schlimmsten Fall ist also jeder Funktionsaufruf theoretisch unsicher! Stellen Sie sich vor, diese Aufruffunktion wäre nicht von jemandem aus meinem eigenen Entwicklungsteam geschrieben worden, sondern würde Open-Source-Code von Drittanbietern im Internet verwenden ... Ich kann mir wirklich nicht vorstellen, wie lange es dauern würde, diesen Fehler zu finden.

3. Goroutine-Missbrauchsfalle

Sehen Sie sich dieses Beispiel an:

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

Es ist erstaunlich, Goroutine ist überall, es schien im Handumdrehen aus dem Nichts auftauchen. Das ist der Missbrauch von go. Wir sehen es überall, aber es ist nicht ganz klar, wo wir go verwenden sollen. Warum Go verwenden? Wird Goroutine wirklich die Effizienz verbessern?

Parallelität in der C-Sprache ist viel komplizierter und umständlicher als die Parallelität in der Go-Sprache, daher werden wir vor der Verwendung gründlich darüber nachdenken und die Vor- und Nachteile der Verwendung von Parallelität abwägen.

Wie man damit umgeht

Hier sind ein paar Möglichkeiten, wie ich mit diesen Problemen umgehe:

1 . Wenn Sie eine Goroutine verwenden und eine Funktion einen Zeiger übergeben muss, die Funktionsebene jedoch sehr tief ist und die Sicherheit nicht gewährleistet werden kann, übergeben Sie den Zeiger an einen Klon des Objekts, anstatt den Zeiger direkt zu übergeben

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

Die Klonfunktion muss separat geschrieben werden. Sie können dieser Methode einfach folgen, nachdem die Struktur definiert wurde. Zum Beispiel:

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开发栏目

Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Verwendung von Goroutine in der Go-Sprache. 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