ホームページ  >  記事  >  バックエンド開発  >  Go言語でのGoroutineの使い方を詳しく解説

Go言語でのGoroutineの使い方を詳しく解説

尚
転載
2019-11-25 14:34:113155ブラウズ

Go言語でのGoroutineの使い方を詳しく解説

go の goroutine は、言語レベルで同時実行性をサポートする go language の機能です。初めて Go に触れたとき、Go の goroutine に非常に満足しました。並行性の実装はばかばかしいほど簡単です。

しかし、プロジェクトのプロセス中に、Goroutine は誰でも簡単に悪用できるものであることがますますわかりました。ゴルーチンは両刃の剣です。 goroutine を使用することの罪は次のとおりです:

1. goroutine で渡すポインタは安全ではありません

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

非常に論理的なコード :

メイン ルーチンは、リクエストを saveRequestToRedis1 に渡すルーチンを開き、リクエストを Redis ノード 1 に保存できるようにします。

同時に、別のルーチンを開いてリクエストを saveReuqestToRedis2 に渡します。リクエストを保存できます。redis ノード 2 に移動します。

その後、メイン ルーチンが (プロセスを終了せずに) ループに入ります。

ここで問題が発生します。saveRequestToRedis1 と saveReuqestToRedis2 の 2 つの関数は、実際には存在しませんでした。誰かが書いたものですが、その実装については何も知りませんし、特定の内部実装を詳しく調べたくありません。しかし、関数名に従って、それを当然のこととして、リクエスト ポインタを渡しました。

実際、saveRequestToRedis1 と saveRequestToRedis2 は次のように実装されています。

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

これの何が問題なのでしょうか? 2 つのゴルーチン saveRequestToRedis1 と saveReuqestToRedis2 は同じ共有データ構造を変更しますが、ルーチンの実行には順序がないため、request.ToUsers 設定と redis.Save() がアトミックな操作であることは保証できません。データエラーバグが発生します。

そうですね、この saveRequestToRedis 関数の実装には問題があり、go ルーチンを使用して呼び出されるということは考慮されていなかったと言えます。 saveRequestToRedis の具体的な実装は問題ありませんが、上位層での使用方法は考慮しないでください。

それは、私の goroutine の使用に問題があるためです。メイン ルーチンがルーチンを開くとき、ルーチン内のコードがメイン ルーチンのデータを変更したかどうかは確認されません。はい、メインルーチンはこの状況を考慮する必要があります。

メインのゴルーチンが go ルーチンを有効にすると、サブルーチン内のコードの各行を読み取って、共有データが変更されたかどうかを判断する必要があります。 ?実際のプロジェクト開発プロセスでは、これがどれほど開発スピードを遅らせることになるでしょうか。

Go 言語は同時開発のプレッシャーを軽減するために goroutine を使用していますが、それが逆に開発のプレッシャーを高めることになるとは考えていませんでした。

上記では多くのことを述べてきましたが、結論としては次のとおりです:

gotine ポインタの受け渡しは安全ではありません。 !

前の例が十分にわかりにくい場合は、別の例を示します:

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

このポインターが指すオブジェクトに問題があるかどうかを考える人はほとんどいないでしょう。ここでの this ポインターは次のとおりです。ルーチンに渡されます。これは非常に隠蔽されていると言うべきです。

2. Goroutine は関数の危険因子を増加させます

この点は、実際には上記の点から派生しています。上で述べたように、Go 関数にポインターを渡すのは安全ではありません。では、別の角度から考えてみましょう。呼び出したい関数が関数実装内で使用されないことをどのように保証できるでしょうか?関数本体内の特定の実装を見ない限り、それを判断する方法はありません。

たとえば、上記の典型的な例を少し変更すれば、

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

同時実行性を使用していないので、この問題は絶対に発生しませんよね。私は関数を追いかけましたが、唖然としました。

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

ゴルーチンが内部で開始され、リクエスト ポインタが指すオブジェクトを変更しました。ここでエラーが発生しました。まあ、関数を呼び出すときに関数内の特定の実装を見ない限り、この問題は回避できません。

つまり、最悪の観点から見ると、すべての関数呼び出しは理論的には安全ではありません。想像してみてください。この呼び出し関数が私自身の開発チームの誰かによって書かれたのではなく、インターネット上のサードパーティのオープン ソース コードを使用していたとしたら...このバグを見つけるのにどれくらいの時間がかかるか、本当に想像できません。

3. Goroutine 悪用トラップ

この例を見てください:

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

すごいですね、go はどこにでもあるようです瞬きする間にどこからともなく現れること。これは go の乱用です。go はどこにでもありますが、あまり明確ではありません。どこで go を使用すればよいでしょうか?なぜ go を使うのでしょうか? goroutine は本当に効率を向上させるのでしょうか?

C 言語の同時実行は Go 言語の同時実行よりもはるかに複雑で面倒なので、使用する前によく考え、同時実行を使用するメリットとデメリットを検討します。

処理方法

これらの問題に対処するための私の方法をいくつか紹介します:

1.開始ゴルーチンを使用する場合、関数がポインタを渡す必要があるが、関数レベルが非常に深く安全性が保証できない場合は、ポインタを直接渡すのではなく、オブジェクトのクローンにポインタを渡します

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

Clone関数は別途記述する必要があります。構造を定義した後は、この方法に簡単に従うことができます。例えば:###

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

以上がGo言語でのGoroutineの使い方を詳しく解説の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。