このところ寄り道をして眺めていっている話題、 データレース
と レースコンディション
まわりについての最後のところ、そのまわりを軽く調べていたときに興味を惹かれた技術ブログを眺めてみようと思います。前回にも調べてみた話題になりますけれど、ブログをたよりにもう少し具体的に様子を窺っていけそうな気がしますので、どうぞよろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #223
00:00 開始 01:36 データ競合と競合状態とは異なる概念 05:54 データ競合は低レイヤーの事象 06:05 プリミティブ? 06:46 データ競合が起こると結果は予測不能に 07:22 同時アクセスを防ぐことで回避する 07:59 モニターという同期機構 10:03 不変条件の考え方 11:14 とりあえずは Actor に近いと捉えておきつつ 13:00 Rust と Swift にみるデータ競合の扱い 14:40 そして話は振り出しの所有権に戻る⋯ 15:57 Rust 言語という言葉をよく聞く印象 17:25 何かを引き合いに出すと、理解の妨げにつながることも 19:03 可変と不変の価値観は大切 19:40 データ競合の定義 22:54 読込-更新-書出操作 23:32 競合状態とは 24:49 クリティカルセクションを不可分とする 28:26 Int 型に対する不可分操作 33:11 クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #223
では勉強会を始めていきます。今日は3回目の脱線テーマとして、データ競合(データレース)と競合状態(レースコンディション)について話します。このテーマについてはあまり馴染みがない人もいるかもしれませんが、基本を押さえておくと、実際に必要になったときに役立ちます。この勉強会の時間を活用して、しっかりと学んでいきましょう。
さて、まずはこのブログを読んでみたいと思います。ブログの信憑性は完全には保障できませんが、参考にする価値はありそうです。データ競合と競合状態を混同しないようにしましょうという話ですね。前回の勉強会でも似たような話題を扱いました。
データ競合とは、同時にアクセスすることでデータが壊れることを意味します。一方、競合状態とは、タイミングによって計算結果が異なる状態のことを指します。これらは直交する異なる概念であり、混同してはいけません。ある場面では区別が不要なこともありますが、通常は別々に考えるべきです。
具体例として、JavaやC++、Swiftのようなマルチスレッド対応プログラミング言語の文脈でこの問題を考えます。データ競合が生じると、プログラムの実行結果は予測不可能になります。たとえば、C++では未定義動作を引き起こすことがあります。C#などでも何が起こるかについての保証はほとんどないです。Swiftでも同様に、不確定な実行結果になる可能性があります。
これを防ぐためには、同時アクセスを制御する必要があります。具体的には、ミューテックスやロック、モニターといった同期機構を使用します。モニターという同期機構については、少し専門的な知識が必要ですが、この勉強会の中で少し掘り下げていくことも考えています。 これはリソースを複数のパスで共有するためのコンセプトです。モニターを使うことで、共有リソースを操作する際の状態を制御します。モニターは普遍条件を仮定しており、何か操作をする前にロックをかけて、終了するか条件が成り立つまで待ちます。これで、ロックを開放する際に普遍条件が真であることを保障し、要望状態となるようなリソースの状態がアクセス不可能になるという仕組みです。
具体的には、ロックをかけ、準備が完了するかある条件が成り立つまでロックを保持します。ロックを開放する際に、普遍条件が真になることを保障するなら、要望状態となるようなリソースの状態はアクセスから見えないということです。
例えば、トランザクションのためのモニターを考えてみましょう。モニターの普遍条件は、新たな操作を行う際に、すべての操作がバランスに反映されていることです。これによって、バランスの整合性が維持されるわけですね。普遍条件はバランスコード自身には書かれておらず、コメントに記載されることが多いです。
ロックはプログラム中に明示的に書かなければならない場合もありますが、モニターを使えばより安全性が高まります。アクターモデルに近い概念で、非同期の処理を安全に行うためにこうした仕組みを利用します。
モニターは同期機構として利用され、シンプルなミューテックスとは異なり、より複雑な制御が可能です。これにより、リソースの状態を適切に管理し、並行処理の安全性を確保します。 とりあえず、"イヤススデコ"またはアトミック変数と呼ばれるデータ型があります。これを駆使して同時アクセスを回避しようとしています。また、前回お話ししたロック機構や、APIの機構を使ってデータを保護し、データレースを防ぐことが重要です。ただし、Rustにおいては型システムレベルで対応されています。データレースを引き起こすコードはコンパイルエラーになります。Rustでもプログラマが明示的にコードを記述すれば、データレースが起こる状況を作ることは可能です。
なるほど、色々とありますね。言語仕様的には基本的にデータレースを防ぐという仕様にはなっていないのかな。ただし、さっきの話にもあったアクターのような仕組みが登場する一般的なフレームワークが変わってきています。この勉強会でも前々回、データレースを引き起こそうとしたら、今回の練習でうまく回避されたことがありましたので、そのあたりは適宜考慮が必要です。
基本的には、同時アクセスを考慮する場合には Mutex
やアトミック変数を使います。こういったアトミック性を保証したり、特定のコンパレーションを使ったりすることが一般的です。また、オーナーシップの役割を理解することでデータレースを防ぐ一助になるでしょう。Rustのオーナーシップの話からデータレースの話が発生しましたね。
そういえば、ちょうど今見ているところはRustのオーナーシップに関連する部分でした。順番に話を進めていたらデータレースの話になったようですね。Rustのオーナーシップはデータレースを防ぐための重要な概念です。Rust以外の言語では見られない特徴が多いので理解しておくと良いですね。
ちなみに、Rustの開発者の一人がSwiftを作った方という話もあります。ですので、言語設計の部分で共通する部分がいくつかあります。例えば、Swiftの let
とRustの let
が似ているという点もあります。
これからもRustという言葉がよく出てくるかもしれませんが、理解しやすくするためにも少し調べてみると良いでしょう。最初は難しいかもしれませんが、「オーナーシップ」以外は比較的シンプルな部分も多いです。
それから、発表の際には引き合いに出す例には注意が必要です。具体的な例を挙げるときに、聞く側の理解が追いつかなくなることがあるので、注意して使うようにしましょう。例えば、Swiftを初めて勉強したときも、Objective-Cと照らし合わせて理解しようとした経験があると思います。 そうすると、余計にそれが理解を妨げることになります。したがって、すべてを詳細に説明しようとはしないのですが、気にしたほうがいい点は確かにあります。その上で、やるかやらないか、出すか出さないかを選択するのが大事だなと感じます。
さて、次に進みましょう。データ競合について話をします。ミュータブルとイミュータブルの概念はとても大事です。これは、導入されていない言語でも重要な価値観です。このような機能が言語によって提供されると、しっかり使うことができるので良いですね。
データ競合とは何でしょう。まず、定義を確認しましょう。複数のデータ間で共有する変数、"変数"というのが重要です。同時に読み書きが行われたときに競合が起きます。例えば、書き込んでいる最中に読み込もうとすると、予期しないデータが読めてしまいます。これが基本的なデータ競合です。
データ競合問題は、プログラミング言語が提供する基本データ型に対して起こり得ます。そして、言語の標準ライブラリ型やユーザーデータ型も基本的には基本型に分解できるため、複雑な実装でもデータ競合が発生する可能性があります。しかし、最近の64ビットCPUでは、128ビットの整数型(たとえば Int128
)などが標準的に提供されていないため、基本型でのデータ競合はほとんど起こらないと言えるかもしれません。
また、CPUの1サイクルで書き込めるデータはデータ競合が起こりにくいと考えています。過去にデータ競合を厳密に考えなければならないコードを書いていたときも、その領域では気を付けていましたが、基本的には大丈夫だと思います。
ただし、多くの言語では同時にデータにアクセスできない場合の状況も定義されており、これは非常に低レベルな話です。こういった内容は求めていない場合はさらっと流しても良いでしょう。
例えば、単純なインクリメント操作( i++
)や掛け算の操作( i *= 2
)は読み込み・更新・書き込み操作と呼ばれ、これは書き込み操作の一種として扱われます。これもデータ競合の一例ですね。
次に、競合状態についてです。マルチスレッドプログラムの並行処理やシステム設計に起因するデータ競合とは異なり、競合状態はより複雑です。競合状態が生じると、マルチスレッドプログラムの結果は実行順序に依存し、プログラムの実行毎に異なる結果が出力されることがあります。予測できない動作につながることもありますが、その手順がプログラムによって指示されているため、その手順がどのように割り込むかによって結果が変わります。
意図しない競合状態は、リソースへの操作が不可分でないことが原因である場合が多いです。そのため、この一連の操作をクリティカルセクション、つまり分割されては困る部分として認識し、適切な制御を行うことが重要です。 Swiftにはデータ競合や共合状態をサポートするための言語機能はありません。そのため、その制御はプログラマーに委ねられます。そうですね、普通の意味で理解すると、一つの処理をまとめて行うことが重要です。
たとえば、i++
のようなインクリメント操作はありふれているかもしれませんが、クリティカルセクションと呼ばれる重要な処理の一部です。i
からの読み出しの最中に他の場所が i
に書き込むと、意図しない結果となる可能性があります。これを避けるためには、ロックを使った制御が必要です。SwiftのActorモデルはこのような状況をある程度は扱ってくれますが、完全に防ぐことはできません。
次に、勉強会の後にコード例を確認すると良いと思います。これにより理解を深めることができます。
データ競合や共合状態についてもう少し知りたいという感想もありますね。これらの概念は、プログラミングをしているときに意識していることが多いかと思いますが、具体的な理解が難しい場合もあるでしょう。たとえば、Atomic(アトミック)操作も重要です。どこまでが不可分かによりますが、アトミック操作を保証する std::atomic
というC++の例を参照すると、変数の適切なサイズによって最適化されています。
一部の例では、アトミック操作に対して int
型の値を渡していました。しかし、アトミック処理をしないと、コンテクストスイッチの際に何が起こるか分からないため、ミューテックスを使うべきだとも言われています。アトミック操作だけでは全体の共合状態を回避するのは難しいので、注意が必要です。
最後に、次回までにさらに調査して面白いことがあれば紹介するという流れにします。今回の勉強会はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。