Go言語同時通信

前のセクション「 Go 言語の goroutine 」の学習を通じて、キーワード go の導入により Go 言語での同時プログラミングがシンプルかつエレガントになりますが、並行プログラミングの本質的な複雑さにも注意し、その容易さを常に意識する必要があります。発生する問題に注意してください。

実際、どのプラットフォーム、どのプログラミング言語、どこであっても、同時実行性は大きなトピックです。同時プログラミングの難しさは調整にあり、調整にはコミュニケーションが必要であり、この観点から見ると、同時実行ユニット間のコミュニケーションが最大の問題となります。

エンジニアリングでは、共有データとメッセージという 2 つの最も一般的な同時通信モデルがあります。

データの共有とは、複数の同時実行ユニットがそれぞれ同じデータへの参照を保持し、データの共有を実現することを意味します。共有データは、メモリ データ ブロック、ディスク ファイル、ネットワーク データなど、さまざまな形式にすることができます。実際のエンジニアリング アプリケーションで最も一般的なのは間違いなくメモリであり、これは共有メモリと呼ばれることがよくあります。

まず、C 言語でスレッド間のデータ共有を通常どのように処理するかを見てみましょう。コードは次のとおりです。

 #include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *count();
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

int main()
{
    int rc1, rc2;
    pthread_t thread1, thread2;

    /* スレッドを作成し、それぞれ独立して関数countを実行する */
    if((rc1 = pthread_create(&thread1, NULL, &count, NULL)))
    {
        printf("スレッドの作成に失敗しました: %d\n", rc1);
    }
    if((rc2 = pthread_create(&thread2, NULL, &count, NULL)))
    {
        printf("スレッドの作成に失敗しました: %d\n", rc2);
    }

    /* 全てのスレッドが実行を終えるまで待機する */
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    exit(0);
}

void *count()
{
    pthread_mutex_lock(&mutex1);
    counter++;
    printf("カウンターの値: %d\n",counter);
    pthread_mutex_unlock(&mutex1);
} 

ここで、この C 言語コードを Go 言語コードに直接変換してみます。コードは次のとおりです。

package main
import (
    "fmt"
    "runtime"
    "sync"
)
var counter int = 0
func Count(lock *sync.Mutex) {
    lock.Lock()
    counter++
    fmt.Println(counter)
    lock.Unlock()
}
func main() {
    lock := &sync.Mutex{}
    for i := 0; i < 10; i++ {
        go Count(lock)
    }
    for {
        lock.Lock()
        c := counter
        lock.Unlock()
        runtime.Gosched()
        if c >= 10 {
            break
        }
    }
} 

上の例では、変数 counter を 10 個のゴルーチン間で共有しました。各ゴルーチンが実行されると、カウンターの値が 1 ずつ増加します。 10 個のゴルーチンが同時に実行されるため、コード内のロック変数であるロックも導入しました。 n に対する各操作では、まずロックをロックし、操作の完了後にロックを開く必要があります。

main 関数では、for ループを使用してカウンターの値を常にチェックします (ロックも必要です)。値が 10 に達すると、すべてのゴルーチンが実行されたことを意味し、この時点で main 関数が戻り、プログラムが終了します。

事態は悪化し始めているようだ。このような単純な関数を実装しますが、そのような肥大化して理解しにくいコードを作成します。大規模なシステムに無数のロック、無数の共有変数、無数のビジネス ロジック、エラー処理ブランチがあることを想像してみてください。それは悪夢でしょう。この悪夢は多くの C/ C++開発者が経験していることですが、実際、 JavaやC# の開発者はそれほど優れたものではありません。

Go 言語は同時プログラミングを言語の中心的な利点として採用しているため、このような無力な方法でこのような問題を解決することは確かに不可能です。 Go 言語は、別の通信モデル、つまり、通信方法として共有メモリではなくメッセージ メカニズムを提供します。

メッセージ メカニズムは、各同時実行ユニットが自己完結型で独立しており、独自の変数を持っているとみなしますが、これらの変数は異なる同時実行ユニット間で共有されません。各同時ユニットには、入力と出力が 1 つだけあり、それがメッセージです。これはプロセスの概念に似ており、各プロセスは他のプロセスによって邪魔されることはなく、独自の作業のみを実行する必要があります。異なるプロセスはメッセージによって通信し、メモリを共有しません。

Go言語が提供するメッセージ通信の仕組みをチャネルと呼びますが、チャネルの導入については追跡調査で説明します。

前のセクション「 Go 言語の goroutine 」の学習を通じて、キーワード go の導入により Go 言語での同時プログラミングがシンプルかつエレガントになりますが、並行プログラミングの本質的な複雑さにも注意し、その容易さを常に意識する必要があります。発生する問題に注意してください。

実際、どのプラットフォーム、どのプログラミング言語、どこであっても、同時実行性は大きなトピックです。同時プログラミングの難しさは調整にあり、調整にはコミュニケーションが必要であり、この観点から見ると、同時実行ユニット間のコミュニケーションが最大の問題となります。

エンジニアリングでは、共有データとメッセージという 2 つの最も一般的な同時通信モデルがあります。

データの共有とは、複数の同時実行ユニットがそれぞれ同じデータへの参照を保持し、データの共有を実現することを意味します。共有データは、メモリ データ ブロック、ディスク ファイル、ネットワーク データなど、さまざまな形式にすることができます。実際のエンジニアリング アプリケーションで最も一般的なのは間違いなくメモリであり、これは共有メモリと呼ばれることがよくあります。

まず、C 言語でスレッド間のデータ共有を通常どのように処理するかを見てみましょう。コードは次のとおりです。

 #include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *count();
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

int main()
{
    int rc1, rc2;
    pthread_t thread1, thread2;

    /* スレッドを作成し、それぞれ独立して関数countを実行する */
    if((rc1 = pthread_create(&thread1, NULL, &count, NULL)))
    {
        printf("スレッドの作成に失敗しました: %d\n", rc1);
    }
    if((rc2 = pthread_create(&thread2, NULL, &count, NULL)))
    {
        printf("スレッドの作成に失敗しました: %d\n", rc2);
    }

    /* 全てのスレッドが実行を終えるまで待機する */
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    exit(0);
}

void *count()
{
    pthread_mutex_lock(&mutex1);
    counter++;
    printf("カウンターの値: %d\n",counter);
    pthread_mutex_unlock(&mutex1);
} 

ここで、この C 言語コードを Go 言語コードに直接変換してみます。コードは次のとおりです。

package main
import (
    "fmt"
    "runtime"
    "sync"
)
var counter int = 0
func Count(lock *sync.Mutex) {
    lock.Lock()
    counter++
    fmt.Println(counter)
    lock.Unlock()
}
func main() {
    lock := &sync.Mutex{}
    for i := 0; i < 10; i++ {
        go Count(lock)
    }
    for {
        lock.Lock()
        c := counter
        lock.Unlock()
        runtime.Gosched()
        if c >= 10 {
            break
        }
    }
} 

上の例では、変数 counter を 10 個のゴルーチン間で共有しました。各ゴルーチンが実行されると、カウンターの値が 1 ずつ増加します。 10 個のゴルーチンが同時に実行されるため、コード内のロック変数であるロックも導入しました。 n に対する各操作では、まずロックをロックし、操作の完了後にロックを開く必要があります。

main 関数では、for ループを使用してカウンターの値を常にチェックします (ロックも必要です)。値が 10 に達すると、すべてのゴルーチンが実行されたことを意味し、この時点で main 関数が戻り、プログラムが終了します。

事態は悪化し始めているようだ。このような単純な関数を実装しますが、そのような肥大化して理解しにくいコードを作成します。大規模なシステムに無数のロック、無数の共有変数、無数のビジネス ロジック、エラー処理ブランチがあることを想像してみてください。それは悪夢でしょう。この悪夢は多くの C/ C++開発者が経験していることですが、実際、 JavaやC# の開発者はそれほど優れたものではありません。

Go 言語は同時プログラミングを言語の中心的な利点として採用しているため、このような無力な方法でこのような問題を解決することは確かに不可能です。 Go 言語は、別の通信モデル、つまり、通信方法として共有メモリではなくメッセージ メカニズムを提供します。

メッセージ メカニズムは、各同時実行ユニットが自己完結型で独立しており、独自の変数を持っているとみなしますが、これらの変数は異なる同時実行ユニット間で共有されません。各同時ユニットには、入力と出力が 1 つだけあり、それがメッセージです。これはプロセスの概念に似ており、各プロセスは他のプロセスによって邪魔されることはなく、独自の作業のみを実行する必要があります。異なるプロセスはメッセージによって通信し、メモリを共有しません。

Go言語が提供するメッセージ通信の仕組みをチャネルと呼びますが、チャネルの導入については追跡調査で説明します。