# 複数の接続に対応するには? サーバー側の現在の実装では、1つのクライアントとの接続しか処理できないことがわかりました。 ではこのような問題に対応するにはどうしたらいいでしょうか。 ## 複数のクライアントに対応する方法 1つのサーバーで複数のクライアントからの接続を同時に処理するためには、以下の方法があります。 - **プロセスを使う**: 各クライアントの接続を別々のプロセスで処理します。これにより、各プロセスが独立して動作し、同時に複数のクライアントを処理できます。 - **スレッドを使う**: 各クライアントの接続を別々のスレッドで処理します。これにより、同時に複数のクライアントからのリクエストを処理できます。 - **非同期I/Oを使う**: Pythonの`asyncio`モジュールを使って、非同期にクライアントの接続を処理します。これにより、I/O待ちの間も他のクライアントの処理が可能になります。 プロセスやスレッドは、OSにおけるプログラムの実行単位というレベルの話になります。 プロセスは、それぞれが独立した状態です。 そのため、プロセス間でデータの共有や受け渡しを行うためにはなんらかの準備が必要となります。 逆に言えばそれだけ独立性が高く、1つのプロセスがクラッシュしても他のプロセスには影響しにくい構造になっています。 スレッドは、同じプロセス内で小分けにされたような感じで考えてください。 そのためスレッド間のデータはプロセス内で共有された形になります。 処理はしやすくなる反面、共有しているデータに対するアクセス方法[^thread-safe]に注意が必要です。 ```{mermaid} sequenceDiagram participant Thread1 as スレッド1 participant Thread2 as スレッド2 participant Shared as 共有データ Thread1->>Shared: 書き込み(更新) Thread2->>Shared: 読み取り(同時アクセス) Note over Thread1,Thread2: レースコンディション発生 ``` ```{note} レースコンディション: 複数のスレッドが同時に共有データにアクセスし、予期しない結果を引き起こす状態を指します。 ``` [^thread-safe]: 具体的には共有しているデータ(メモリ)に対する書き込みが該当します。どこかのスレッドで書き込み中に他のスレッドが読み書きすると、データが壊れる可能性があります。 レースコンディションを防ぐためには、共有するデータに対して『今は書き込み禁止』『今は書き込み可能』といったフラグのようなものを用意する必要があります。 この手の技術としては、ミューテックス(Mutex)やセマフォ(Semaphore)などがあります。 言語によっては特定の操作の前後にロックをかけるといった方法も用意されることがあります[^lock]。 [^lock]: Pythonでは`threading.Lock`を使うことができます。Javaだと`synchronized`キーワードを使うことができます。 非同期I/Oは比較的最近の技術で、Pythonでは`asyncio`モジュールを使うことで実現できます。 ## マルチプロセス型のサーバー ここでは比較的実装の簡単な、マルチプロセス型のサーバーを実装してみます。 マルチプロセス型のサーバーは、各クライアントの接続を別々のプロセスで処理するため、各プロセスが独立して動作します。 ```{literalinclude} source/calc_server_multiprocess.py :language: python :linenos: ``` 少しわかりにくいですね、変更点を出すとこうなります。 ```{literalinclude} source/calc_server_multiprocess.py :language: python :linenos: :diff: source/calc_server.py ``` 見ての通りで、接続状態を保持する {code}`conn`を利用して、{code}`multiprocessing.Process`を使って新しい子プロセスを生成しています。 現代OSの基礎概念のひとつで、プロセスからプロセスを生成する際の考え方があります。 自身(実行中のプロセス自身)のコピーをコピーするもので、UNIX(Linux)界隈では`fork`と呼ばれています。 ```{mermaid} flowchart TD subgraph 親プロセス P[カーネル空間] A[ユーザープロセス] end P -- fork要求 --> K[カーネル] K -- プロセス複製 --> C[子プロセス] C -.->|独立して実行| A style K fill:#f9f,stroke:#333,stroke-width:2px style C fill:#bbf,stroke:#333,stroke-width:2px style A fill:#bfb,stroke:#333,stroke-width:2px ``` ```{note} Windowsでは`fork`は存在しません、代わりに`CreateProcess`というAPIが存在します。 これは、子プロセスを生成することに変わりませんが、親プロセスの状態をコピーするわけではありません。 実際に起動させたいプログラムを指定して起動させます。 ``` forkでは親プロセスの状態をコピーして動くため、『開いているファイル』や『開いているソケット』などもコピーを持つことになります。 そこで、コピー元(親プロセス)側は即座にソケット(接続)を閉じておくことにしましょう。これで『なぜか二股になっていた』状態が解消します。 そして`while`のループの冒頭に戻り、次の `sock.accept()` で次の接続を待つようになります。