ホーム 主筆 その他ソフト その他情報 Syuhitu.org English

サービス分割

<< 「マルチスレッド開発の傾向と対策」に戻る

概要

Socket等を用いてサービスを提供するプロセス、すなわちサーバプロセスの実装で用いられるマルチスレッド化手法である。

クライアントからアクセスがある度に、当該のクライアントに対応するためのスレッドを生成し、そのスレッドが責任を持って当該のクライアントからの要求を処理するという方式である。

理論

Socket等の入出力ポートを、ある1つのスレッド(メインスレッド)で監視して、クライアントからのアクセスを待ち合わせる。 そしてアクセスがあったら、そのクライアントの面倒を見るためのスレッド(サービススレッド)を生成し、全ての処理を丸投げする。

メインスレッドは、サービススレッドを生成したら、すぐにアクセス監視処理に戻る。サービススレッドはコネクションが終了したら、それに併せて消滅する。

なぜ、このような構成にするのか。どうして、クライアントからのアクセスを受け付けたメインスレッドで、そのままクライアントに対して応答してはいけないのか。

その理由は、メインスレッドでそのままクライアントに応答するような方式にした場合に何が起きるのかを考えると判りやすい。

もし、単純にシングルスレッドで処理するようにした場合、同時に処理できるクライアントの数は最大でも1つに限定されてしまう。 つまり、あるクライアントに対する処理を行っている間は、他のクライアントからのアクセスを受け付ける事ができなくなってしまう。

全てのクライアントからのアクセスを、完全にシリアライズして処理したのであれば、このような構成にするのが妥当であるが、一般的にはそういう需要はない。 また、クライアントにサービスを提供する処理に要する時間は、CPUの処理時間以外にもHDDやネットワークの処理時間も含まれる。 そのため、複数クライアントの処理を同時に捌くことによって、コンピュータが持つリソースの利用効率が向上し、レスポンスのみならずスループットも向上することが可能となる。 これは、利用可能なCPUが1つしかない場合にも当てはまる。

そのため、多少面倒でも、幾分構造が複雑になったとしても、クライアントからのアクセスを受け付ける都度、スレッドを生成するようにするのである。

なお、実用的なサーバ製品などでは、単純に1クライアントを1スレッドで処理するのではなく、1スレッドで複数のクライアントを処理するような構成となっているらしい。 これは、スレッド生成処理に要する時間などを考慮に入れ、最適化を図っているためである。

実装

一番一般的だからという理由で、TCP/IPのサーバプロセスを例に考えてみる。

#include <socket.h>
#include <stdio.h>
#include "Thread.h"

class CFoo : public CThread
{
public:
  CFoo() {};

  // スレッドのエントリポイント
  void run()
  {
    // 対クライアントの処理を行う
    ……
    // ソケットを閉じる
    close( sh );
    // 自分自身を破棄する
    delete this;
  };
  int sh;  // ソケットのディスクリプタ
};

int main() {
  ……

  // 無限ループ
  while( -1 ) {
    // 接続を待ち受ける
    sh = socket( IF_INET, SOCK_STREAM, 0 );
    ……
    // 接続してきたクライアントを捌くための
    // スレッドを生成する
    p = new CFoo();
    p->sh = sh;
    p->start();
  }
  …
}

まず、socket関数を呼び出し、クライアントからの接続を待ち受ける。その後、接続してきたクライアントを捌くためのスレッドの生成を行っている。

ここでは、CFooクラスのメンバshにソケットのディスクリプタを設定して、startを呼び出すことにより、ディスクリプタの引き渡しとスレッドの生成を実現している。

また、CFooクラスのインスタンスはスレッド1つに付き1つずつ生成するようにしている。そのため、CFoo::runの最後に「delete this」とすることで、自分自身のインスタンスを破棄している。(delete thisが気持ち悪いのなら、CFooのインスタンスを各スレッドで共有する方法を使用しても良いだろう。)

接続を待ち受けるスレッド(whileで無限ループしているスレッド)は、CFoo::startを呼び出してスレッドを生成してしまうと、そのままsocket関数を呼び出して、次の接続を待ち受ける体勢に入る。これにより、次のクライアントは前のクライアントの処理の終了を待つことなく、サービスを受けることが可能となる。

利用

これは、ある一つの入出力ポートからのデータの入力を待ち受け、その入力に対して応答を返さなければならず、かつ、データの入力が連続的・同時並行的に発生しうる場合に利用される。

言葉にすると判りにくいので、絵で表してみる。

まず、1つの入出力ポートに対して複数のクライアントがアクセスしてくる場合であり、

かつ、全てのクライアントに対して、同時並行的に(すなわち時間的に重なりがある状態で)応答を返さなければならない場合に、サービス分割が使用される。

具体的には、Webサーバのプロセス等を考えれば判りやすい。

だが、ある瞬間に1つの入出力ポートを1つのクライアントが占有する場合には、サービス分割は使用されない。

例えば、入出力ポートとして生のCOMポートを使うような場合などが該当する。

この場合は、アクセスを待ち合わせるスレッドが入出力ポート数分必要になるため、サービス分割は利用できないのである。

ただし、何らかの方法(select関数の利用など)により、複数のポートに対するアクセスを単一のスレッドで監視することができるのであれば、サービス分割が可能なパターンに持ち込むことが可能となる。

<< 「マルチスレッド開発の傾向と対策」に戻る