ホームページ  >  記事  >  バックエンド開発  >  Go 言語でのチャネルの詳細な紹介

Go 言語でのチャネルの詳細な紹介

尚
転載
2019-11-25 16:29:513911ブラウズ

Go 言語でのチャネルの詳細な紹介

この記事は go 言語 でのチャネルについての記事です。なぜこれを書いたかというと、チャネルが非常に重要だと感じているからです。同時に、チャネルはgo の同時実行性の重要なサポート ポイントでもあり、go は通信に共有メモリを使用するのではなく、メッセージ パッシング共有メモリを使用するためです。

同時プログラミングは非常に優れていますが、同時実行は非常に複雑です。難しいのは調整です。さまざまなプログラム間の通信をどのように処理するかが非常に重要です。チャネルの使用法と特性について書く前に、オペレーティング システムのプロセス間通信を確認する必要があります。

#プロセス間通信

エンジニアリングには、共有データとメッセージという 2 つの一般的な通信モデルがあります。名前が示すように、プロセス通信はプロセス間の情報交換を指します。プロセスの相互排除と同期にはプロセス間の情報交換が必要です。オペレーティング システムを勉強したことがある人なら誰でも、プロセス通信が低レベルのプロセスに大別できることを知っています。通信と高度なプロセス通信. さて、基本的に上記はすべて高度なプロセス通信です。その中で、高度な通信メカニズムは、メッセージ パッシング システム、共有メモリ システム、パイプライン通信システム、クライアント サーバー システムに分類できます。

1. メッセージ パッシング システム

共有ストレージ領域や特定のデータ構造に依存せず、フォーマットされたメッセージを単位としてシステムを使用して提供します。データ交換を完了するために通信プリミティブが使用されますが、これは非常に非効率であるように見えます。


2. 共有メモリ システム

通信プロセスは記憶領域またはデータ構造を共有し、これらの空間を介して通信します。この方法は、特定のファイルをキャリアとして使用するなど、比較的一般的です。

3. クライアント サーバー システム

他のいくつかの通信メカニズムは、基本的に同じコンピュータ上にあります (同じ環境であると言えます)。コンピュータ間通信は以下で実現できます。クライアントサーバーシステムは異なりますが、私の理解では、これは IP リクエストとみなされ、クライアントはサーバーへの接続を要求します。

この方法は現在、インターネット上でより一般的です。RPC などのリモート スケジューリングが現在ではより一般的に使用されています (これは非常にハイエンドに聞こえますが、実際には、オペレーティング システムで長い間利用可能でした) ) とソケット、ソケット、これは非常に一般的に使用されており、多くのサービスが RPC 呼び出しを使用する必要があることがわかるため、私たちのプログラミングと密接に関連しています。

4. パイプライン通信部門

最後に、パイプ通信の仕組みについて詳しく説明しますオペレーティング システム レベルでは、パイプを使用して読み取りプロセスをリンクします。ファイル間の通信を可能にする書き込みプロセス。システム上ではパイプファイルと呼ばれます。

実装されたメカニズムは次のとおりです: パイプラインは次の 2 つの機能を提供します

1. 相互排他性: プロセスがパイプ ファイルに対して読み取りまたは書き込み操作を実行しているとき、他のプロセスは待機する必要があります。ブロックするかスリープします。

2. 同期性: 書き込み (入力) プロセスがパイプ ファイルに書き込むとき、読み取り (出力) プロセスがデータの取得後にウェイクアップするまで待機、ブロック、またはスリープします。空のパイプ ファイルを読み取るときも、パイプへの書き込み後に書き込みプロセスが起動するまで待機、ブロック、またはスリープします。

チャネルの使い方

goに対応するチャネルは4番目のタイプになります go言語のチャネルとは、言語レベルで提供されるゴルーチン間の通信手段です。チャネルだけを語るのはゴルーチンと併用すると効果的ですので意味がありませんが、まずは一般的な言語がプログラム間の共有メモリをどのように解決しているかを見てみましょう。

次は、私たちがよく知っているプログラムです:

package main

import "fmt"

var counts int = 0

func Count() {
    counts++
    fmt.Println(counts)
}
func main() {

    for i := 0; i < 3; i++ {
        go Count()
    }
}

Go を学習したことがある人なら、その理由がわかるはずです。なぜなら、Go プログラムは main() メソッドとパッケージを初期化し、次に実行します。 main() 関数は実行されますが、main() 関数が戻るとプログラムは終了します。メイン プログラムは他のゴルーチンを待機しないため、出力はありません。

従来の言語がこの同時実行の問題をどのように解決するかを見てみましょう:

package main

import "fmt"
import "sync"
import "runtime"

var counts int = 0

func Count(lock *sync.Mutex) {
    lock.Lock()
    counts++
    fmt.Println(counts)
    lock.Unlock()
}
func main() {
    lock := &sync.Mutex{}

    for i := 0; i < 3; i++ {
        go Count(lock)
    }

    for {
        lock.Lock()
        c := counts
        lock.Unlock()

        runtime.Gosched()

        if c >= 3 {
            break
        }

    }
}

この解決策は少し面白く、大量のロックを追加します。その実行は次のようなものであるためです。ロック変数を入力するには、カウントに関するすべての操作を最初にロックする必要があります。操作が完了したら、ロックを解除する必要があります。main 関数では、for ループを使用してカウンターの値を継続的にチェックします。もちろんロックは必要です。ロックされることもあります。

値が 3 に達すると、すべてのゴルーチンが実行されたことを意味し、この時点で main 関数がリターンし、プログラムが終了します。この方法は、一般的な言語で同時実行性を解決するための推奨される方法です。同時実行性を解決するために多くのことが書かれていることがわかります。プロジェクトが形になり始めている場合、いくつのロックを追加する必要があるかわかりません。

チャネルがこの問題をどのように解決するかを見てみましょう:

package main

import "fmt"

var counts int = 0

func Count(i int, ch chan int) {
    fmt.Println(i, "WriteStart")
    ch <- 1
    fmt.Println(i, "WriteEnd")
    fmt.Println(i, "end", "and echo", i)
    counts++
}

func main() {
    chs := make([]chan int, 3)
    for i := 0; i < 3; i++ {
        chs[i] = make(chan int)
        fmt.Println(i, "ForStart")
        go Count(i, chs[i])
        fmt.Println(i, "ForEnd")
    }

    fmt.Println("Start debug")
    for num, ch := range chs {
        fmt.Println(num, "ReadStart")
        <-ch
        fmt.Println(num, "ReadEnd")
    }

    fmt.Println("End")

    //为了使每一步数值全部打印
    for {
        if counts == 3 {
            break
        }
    }
}

ゴルーチンの実行手順とチャネルの特性を明確に確認するために、各ステップを特別に印刷しました。以下はその実行です。興味のある学生印刷順序は異なる場合があります:

Go 言語でのチャネルの詳細な紹介

このプロセスを分析して、そのプロセスにおけるチャネルの役割を見てみましょう。メイン プログラムが開始されます:

Print "0 ForStart 0 ForEnd"。これは、i = 0 このサイクルが実行を開始し、最初のゴルーチンが開始したことを示します。

打印 "1 ForStart"、"1 ForEnd"、"2 ForStart"、"2 ForEnd" 说明3次循环都开始,现在系统中存在3个goroutine;

打印 "Start debug",说明主程序继续往下走了,

打印 "0 ReadStar"t ,说明主程序执行到for循环,开始遍历chs,一开始遍历第一个,但是因为此时 i = 0 的channel为空,所以该channel的Read操作阻塞;

打印 "2 WriteStart",说明第一个 i = 2 的goroutine先执行到Count方法,准备写入channel,因为主程序读取 i = 0 的channel的操作再阻塞中,所以 i = 2的channel的读取操作没有执行,现在i = 2 的goroutine 写入channel后下面的操作阻塞;

打印 "0 WriteEnd",说明 i = 0 的goroutine也执行到Count方法,准备写入channel,此时主程序 i = 0 的channel的读取操作被唤醒;

打印 "0 WriteEnd" 和 "0 end and echo 0" 说明写入成功;

打印 "0 ReadEnd",说明唤醒的 i = 0 的channel的读取操作已经唤醒,并且读取了这个channel的数据;

打印 "0 ReadEnd",说明这个读取操作结束;

打印 "1 ReadStart",说明 i = 1 的channel读取操作开始,因为i = 1 的channel没有内容,这个读取操作只能阻塞;

打印 "1 WriteStart",说明 i = 1 的goroutine 执行到Count方法,开始写入channel 此时 i = 1的channel读取操作被唤醒;

打印 "1 WriteEnd" 和 "1 end and echo 1" 说明 i = 1 的channel写入操作完成;

打印 "1 ReadEnd",说明 i = 1 的读取操作完成;

打印 "2 ReadStart",说明 i = 2 的channel的读取操作开始,因为之前已经执行到 i = 2 的goroutine写入channel操作,只是阻塞了,现在因为读取操作的进行,i = 2的写入操作流程继续执行;

打印 "2 ReadEnd",说明 i = 2 的channel读取操作完成;

打印 "End" 说明主程序结束。

此时可能你会有疑问,i = 2 的goroutine还没有结束,主程序为啥就结束了,这正好印证了我们开始的时候说的,主程序是不等待非主程序完成的,所以按照正常的流程我们看不到 i = 2 的goroutine的的完全结束,这里为了看到他的结束我特意加了一个 counts 计算器,只有等到计算器等于3的时候才结束主程序,接着就出现了打印 "2 WriteEnd" 和 "2 end and echo 2"  到此所有的程序结束,这就是goroutine在channel作用下的执行流程。

上面分析写的的比较详细,耐心看两遍基本上就明白了,主要帮助大家理解channel的写入阻塞和读入阻塞的应用。

基本语法

channel的基本语法比较简单, 一般的声明格式是:

var ch chan ElementType

定义格式如下:

ch := make(chan int)

还有一个最常用的就是写入和读出,当你向channel写入数据时会导致程序阻塞,直到有其他goroutine从这个channel中读取数据,同理如果channel之前没有写入过数据,那么从channel中读取数据也会导致程序阻塞,直到这个channel中被写入了数据为止

ch <- value    //写入
value := <-ch  //读取

关闭channel

close(ch)

判断channel是否关闭(利用多返回值的方式):

 b, status := <-ch

带缓冲的channel,说起来也容易,之前我们使用的都是不带缓冲的channel,这种方法适用于单个数据的情况,对于大量的数据不太实用,在调用make()的时候将缓冲区大小作为第二个参数传入就可以创建缓冲的channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

c := make(chan int, 1024)

单项channel,单向channel只能用于写入或者读取数据。channel本身必然是同时支持读写的,否则根本没法用。所谓的单向channel概念,其实只是对channel的一种使用限制。单向channel变量的声明:

var ch1 chan int   // ch1是一个正常的channel
var ch2 <-chan int // ch2是单向channel,只用于读取int数据

单项channel的初始化

ch3 := make(chan int)
ch4 := <-chan int(ch3) // ch4是一个单向的读取channel

超时机制

超时机制其实也是channel的错误处理,channel固然好用,但是有时难免会出现实用错误,当是读取channel的时候发现channel为空,如果没有错误处理,像这种情况就会使整个goroutine锁死了,无法运行。

我找了好多资料和说法,channel 并没有处理超时的方法,但是可以利用其它方法间接的处理这个问题,可以使用select机制处理,select的特点比较明显,只要有一个case完成了程序就会往下运行,利用这种方法,可以实现channel的超时处理:

原理如下:我们可以先定义一个channel,在一个方法中对这个channel进行写入操作,但是这个写入操作比较特殊,比如我们控制5s之后写入到这个channel中,这5s时间就是其他channel的超时时间,这样的话5s以后如果还有channel在执行,可以判断为超时,这是channel写入了内容,select检测到有内容就会执行这个case,然后程序就会顺利往下走了。

实现如下:

timeout := make(chan bool, 1)
go func() {
    time.Sleep(5s) // 等待s秒钟
    timeout <- true
}()

select {
    case <-ch:
    // 从ch中读取到数据
    case <-timeout:
    // 没有从ch中读取到数据,但从timeout中读取到了数据
}

推荐:go语言教程

以上がGo 言語でのチャネルの詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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