stdoutスレッドセーフでの同時書き込みはありますか?

Dec 09 2020

以下のコードはデータ競合をスローしません

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    x := strings.Repeat(" ", 1024)
    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"aa\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"bb\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"cc\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"dd\n")
        }
    }()

    <-make(chan bool)
}

バリアントを使用して、複数の長さのデータを試しました https://play.golang.org/p/29Cnwqj5K30

この投稿は、TSではないと言っています。

このメールは本当に質問に答えていないか、私は理解していませんでした。

osとfmtのパッケージドキュメントはこれについてあまり言及していません。私はこれらの2つのパッケージのソースコードを掘り下げてさらに説明を見つけなかったことを認めます。それらは私には複雑すぎるように見えます。

推奨事項とその参照は何ですか?

回答

6 kostix Dec 09 2020 at 17:10

それが決定的な答えとなるかどうかはわかりませんが、いくつかの洞察を提供しようと思います。

パッケージの-F*関数は、インターフェイスをfmt実装する型の値を取り、それio.Writerを呼び出すことを示しWriteているだけです。関数自体は同時使用に対して安全です—任意の数をfmt.Fwhaveter同時に呼び出すことができるという意味で:パッケージ自体はそのために準備されていますが、Goでのインターフェースのサポートは、並行性に関して実際の型について何も述べていません。

言い換えると、並行性が許可される場合と許可されない場合の実際のポイントは、fmt書き込み機能が書き込む「ライター」に委ねられます。(ストックパッケージで提供されるfmt.*Print*関数とはWrite対照的に、関数は宛先を何度でも呼び出すことができることにも注意してくださいlog。)

したがって、基本的に2つのケースがあります。

  • のカスタム実装io.Writer
  • パッケージ*os.Fileの関数によって生成されたソケットのラッパーなど、そのストック実装net

最初のケースは単純なケースです。実装者が行ったことは何でもです。

2番目のケースはもっと難しいです:私が理解しているように、これに対するGo標準ライブラリのスタンス(ドキュメントには明確に記載されていませんが)は、OSによって提供される「もの」(ファイル記述子やソケットなど)の周りにラッパーが提供するという点で合理的です「薄い」、つまりそれらが実装するセマンティクスはすべて、特定のシステムで実行されているstdlibコードによって推移的に実装されます。

たとえば、POSIXでは、write(2)呼び出しが 通常のファイルまたはシンボリックリンクで動作している場合、呼び出しが相互アトミックである必要があります。つまり、Writeファイル記述子またはソケットをラップするものを呼び出すと、実際にはタグレットシステムの単一の「書き込み」システムコールが発生するため、ターゲットOSのドキュメントを参照して、何が起こるかを理解することができます。

POSIXはファイルシステムオブジェクトについてのみ通知os.Stdoutし、ターミナル(または疑似端末)、パイプ、またはwrite(2)syscallをサポートするその他のものに対して開かれている場合、結果は関連するサブシステムやドライバーによって異なります。実装—たとえば、複数の同時呼び出しからのデータが散在している、または呼び出しの1つ、あるいはその両方がOSによって失敗する可能性があります—可能性は低いですが、それでもなおです。

Goに戻ると、私が収集したことから、ファイル記述子とソケットをラップするGostdlibタイプについて次の事実が当てはまります。

  • それらは、それ自体で同時に使用しても安全です(つまり、Goレベルで)。
  • それらは、基礎となるオブジェクトに1対1で「マッピング」WriteしてRead呼び出します。つまり、Write呼び出しが2つ以上の基礎となるシステムコールに分割されることはなく、呼び出しがRead複数の基礎となるシステムコールの結果から「接着」されたデータを返すことはありません。(ちなみに、人々はこの飾り気のない振る舞いにつまずくことがあります。たとえば、これまたはこれを例として見てください。)

したがって、基本的にこれを事実と見なすと、1回のfmt.*Print*呼び出しでWrite何度でも自由に呼び出すことができます。を使用する例ではos.Stdout、次のようになります。

  • 変数にos.Stdoutカスタム実装を割り当てていない限り、データの競合が発生することはありませんが、
  • 基盤となるFDに実際に書き込まれるデータは、予測できない順序で混在します。これは、OSカーネルのバージョンと設定、プログラムのビルドに使用されるGoのバージョン、ハードウェア、システムの負荷など、多くの要因に依存する可能性があります。

TL; DR

  • fmt.Fprint*同じ「ライター」値への書き込みに対する複数の同時呼び出しは、「ライター」の実装(タイプ)への同時実行を延期します。
  • 質問で提示したセットアップで、Gostdlibによって提供される「ファイルのような」オブジェクトとのデータ競合を起こすことは不可能です。
  • 本当の問題は、Goプログラムレベルでのデータの競合ではなく、OSのレベルで発生する単一のリソースへの同時アクセスにあります。そして、そこでは(通常)データ競合については話しません。OSesGoがサポートするコモディティは、抽象化として「書き込む」可能性のあるものを公開するためです。実際のデータ競合は、カーネルまたはドライバー(およびGoの競合検出器は、そのメモリがプロセスに電力を供給するGoランタイムによって所有されないため、とにかくそれを検出できません。

基本的に、あなたの場合、特定の呼び出しによって生成されたデータfmt.Fprint*が、OSによって提供される実際のデータ受信者に単一の連続した部分として出力されることを確認する必要がある場合、fmtパッケージはに関する保証を提供しないため、これらの呼び出しをシリアル化する必要がありますWriteエクスポートする関数に対して提供された「ライター」での呼び出しの数。
シリアル化は、外部(明示的、つまり「ロックの取得、呼び出しfmt.Fprint*、ロックの解放」)または内部のいずれか(ロックos.Stdoutを管理するカスタムタイプでラップして使用することによる)のいずれかです。そして、私たちがそれに取り組んでいる間、logパッケージはまさにそれを行い、デフォルトのものを含め、それが提供する「ロガー」としてすぐに使用でき、「ログヘッダー」(タイムスタンプや名前など)の出力を禁止できますファイルの)。