yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

OLE Drag & Dropを実装する

この記事は2001年頃に書いた文章をそのまま転記し、はてなブログ用に体裁を整えたものです。Windows2000+Win32 SDKC言語を前提としており、2012年現在では内容が陳腐化している箇所もあります。

(当時の)ソースコードはこちら: https://gist.github.com/3053385


目的

Windowsの便利な機能に、ドラッグ・アンド・ドロップ(以下D&D)によるファイルのコピー・移動・ショートカット作成があります。これに相当する機能を実装してみようというのが今回の目的です。
ちなみに開発環境は、Win32 SDKベースのC言語を想定しています。

非OLE Drag&Drop

Explorer等からのファイルD&Dを実装する際に、最も簡単なのはDragAcceptFiles()関数を用いる方法です。
ファイルのドロップを受け付けたいウインドウのウインドウハンドル(hwnd)とすると、

HWND hwnd = /* 対象ウインドウのHWND */;

DragAcceptFiles(hwnd, TRUE);

とすることで、ファイルのドロップを受け付けるようになります。
対象ウインドウにExplorerからファイルがD&Dされると、ウインドウプロシージャーにWM_DROPFILESが送られるので、このメッセージをフックしてドロップされたファイルを処理してやります。

LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
  )
{
  HDROP hdrop;
  char filename[MAX_PATH];
  int num, i;

  switch (uMsg) {
  case WM_DROPFILES:
    hdrop = (HDROP)wParam;
    /* ドロップされたファイルの個数を得る */
    num = DragQueryFile(hdrop, -1, NULL, 0);
    /* ファイルを処理 */
    for (i = 0; i < num; i++) {
      DragQueryFile(hdrop, i, filename, &filename);
      /*
       * filename にファイル名が入っているので、ここで処理を行う。
       */
    }
    DragFinish(hdrop);
    break;

  /* other messages... */
  }
  return 0;
}

これでExplorerからのファイルドロップを実装できました。しかし、まだファイルD&Dのうち後者しか実現していません。
次はExplorerへのファイルドラッグの実装に挑戦してみます。

OLE

“ドラッグ”は“ドロップ”と対になっている動作ですが、Explorerへのファイル“ドラッグ”に関しては残念ながら上記のように単純には実装できません。これを行うにはOLE(Object Linking and Embedding)を利用する必要があります。OLEはCOM(Component Object Model)技術に基づいているため、実際はCOMサーバーを実装することが必要になってきます。
ここで問題になるのは、COMがオブジェクト指向であるためC言語で実装するのが面倒だということです。C++ならばclassを利用して比較的楽に実装できるのですが、Cにはそのような言語的サポートはありません。よって少々煩雑なコーディングが必要になってきます。
以下の解説ではCOMに関する説明は省略します。COMについて上手に説明しているサイトは沢山ありますのから、そちらを参考にして下さい。

OLE Drag&Drop

さて、OLE D&Dを実装するにあたって次の3つのCOMインターフェイスを使う必要があります。

  • IDataObject
  • IDropSource
  • IDropTarget

IDataObjectは受け渡しするデータ(ここではファイル)であり、IDropSourceはOLE D&Dのドラッグ開始元に、またIDropTargetはOLE D&Dのドロップ先に対応しています。そして、OLE D&Dのドラッグ開始するにはDoDragDrop()関数を用います。
OLE D&Dにおける処理の流れは次のようになっています。


  1. DoDragDrop()関数を呼び出す。
  2. ドラッグ中のオブジェクトがウインドウ領域の上に入ると、IDropTarget::DragEnter()が呼び出される。
  3. IDropSource::GiveFeedback()が呼び出される。
  4. マウスがウインドウ領域の上にいる間は次の関数が呼び出された後、IDropSource::GiveFeedback()が呼び出される。

    • マウスカーソルが領域内で動くと、IDropTarget::DragOver()が呼び出される。
    • マウスカーソルがウインドウ領域外に移動すると、IDropTarget::DragLeave()が呼び出される。

  5. ドラッグ中にキー入力やマウスクリックが発生するとIDropSource::QueryContinueDrag()が呼び出される。ここで、D&Dを完了させる(ドロップ)かキャンセルするかを決定する。
  6. オブジェクトがドロップされると、IDropTarget::Drop()が呼び出される。
  7. DoDragDrop()関数から戻る。このときIDropTarget側で設定されたDROPEFFECTが戻り値として得られる。

ここで、IDropSourceとIDropTargetは別々のアプリケーションに属している(ことが多い)に注意してください。
例えばExplorerからのファイルD&Dの場合、IDropSourceはExplorer側で提供され、IDropTargetは自作アプリケーション側で提供したものが使われます。

IDropSource

まず、ファイルの“ドラッグ”を実現するためのIDropSourceインターフェイスを実装します。IDropSourceには次の2つのメソッドがあります。

  • IDropSource::GiveFeedback()
  • IDropSource::QueryContinueDrag()

GiveFeedback()メソッドではドラッグ先(IDropTarget)で指定されたD&Dの状態(DROPEFFECT)によって、SetCursor()関数等でマウスカーソルにフィードバックを与えます。このときDRAGDROP_S_USEDEFAULTCURSORSを返すことで、OLEがデフォルトで提供するカーソル形状を利用できます。
QueryContinueDrag()メソッドではキー入力やマウス入力によって、D&Dを完了するかキャンセルするかを返します。Windowsの一般的なGUIとしては、ESCが押されたらキャンセルしマウスボタンが離されたらD&D完了(オブジェクトのドロップ)とします。

typedef struct tagOLEDropSource {
  IDropSource super;
  ULONG       m_cRefCnt;
} OLEDropSource;

/* IUnknown methods */
STDMETHODIMP OLEDropSource_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropSource_Release(/*略*/);

/* IDropSource methods */
STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  BOOL fEscapePressed,
  DWORD grfKeyState
  )
{
  /* ESCキーが押されたらD&Dキャンセル */
  if (fEscapePressed)
    return DRAGDROP_S_CANCEL;
  /* マウスボタンが離されたらD&D完了 */
  if (!(grfKeyState & (MK_LBUTTON|MK_RBUTTON)))
    return DRAGDROP_S_DROP;
  return S_OK;
}

STDMETHODIMP OLEDropSource_QueryContinueDrag(
  OLEDropSource *this,
  DWORD dwEffect
  )
{
  /* OLEデフォルトのカーソルを使用する */
  return DRAGDROP_S_USEDEFAULTCURSORS;
}
IDataObject

次にOLE D&Dで受け渡されるデータを表現するためのIDataObjectインターフェイスを実装します。IDataObjectには次の9つのメソッドがあります。

  • IDataObject::GetData()
  • IDataObject::GetDataHere()
  • IDataObject::QueryGetData()
  • IDataObject::GetCanonicalFormatEtc()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()
  • IDataObject::DAdvise()
  • IDataObject::DUnadvise()
  • IDataObject::EnumDAdvise()

これら全てのメソッドを実装するのは大変な作業なのため、ここではファイルを表すHDROPを格納するための最低限の実装だけ行うことにします。
実装するのは次の4つのメソッドと、EnumFormatEtc()メソッドで必要になるIEnumFORMATETCインターフェイスです。

  • IDataObject::GetData()
  • IDataObject::QueryGetData()
  • IDataObject::SetData()
  • IDataObject::EnumFormatEtc()

ここではIDataObject/IEnumFORMATETCの実装の表記は省略します。

OLE D&D: Drag編

ではExplorerへのファイルドラッグのための前準備からD&D開始、後処理までの流れをコーディングします。

/*
 * ファイル名を'\0'で区切った文字列。末尾は2個の'\0'で終わる。
 */
char *files = "c:\file1.txt\0c:file2.txt\0\0";

IDataObject *lpDataObject = /* IDataObjectのインスタンス */;
IDropSource *lpDropSource = /* IDropSourceのインスタンス */;
FORMATETC fmtetc;
STGMEDIUM medium;
char *p;
DWORD effect;
HRESULT hr;


/* FORMATETC構造体をセット */
fmtetc.cfFormat = CF_HDROP;
fmtetc.ptd = NULL;
fmtetc.dwAspect = DVASPECT_CONTENT;
fmtetc.lindex = -1;
fmtetc.tymed = TYMED_HGLOBAL;

/* STGMEDIUM構造体をセット */
medium.tymed = TYMED_HGLOBAL;
medium.hGlobal = GlobalAlloc(
                   GMEM_MOVEABLE, 
                   sizeof(DROPFILES) + sizeof(files)
                 );
medium.pUnkForRelease = NULL;
p = GlobalLock(medium.hGlobal);
((DROPFILES *)p)->pFiles = sizeof(DROPFILES);
((DROPFILES *)p)->fWide = FALSE;
CopyMemory(p + sizeof(DROPFILES), files, sizeof(files));
GlobalUnlock(medium.hGlobal);

/* IDataObjectをセット */
IDataObject_SetData(lpDataObejct, &fmtetc, &medium, FALSE);

/* OLE Drag & Drop開始 */
hr = DoDragDrop(
       lpDataObejct,
       lpDropSource,
       DROPEFFECT_MOVE|DROPEFFECT_COPY|DROPEFFECT_LINK,
       &effect
     );

/* リソースを開放 */
IDataObject_Release(lpDataObejct);
IDropSource_Release(lpDropSource);

/*
 * hr, effectの値によってデータを処理...
 */

実際のコードはマウスのドラッグ開始メッセージなどによって実行されるようにします。これはWM_LBUTTONDOWN, WM_RBUTTONDOWN, LVN_BEGINDRAG(リストビュー), TVN_BEGINDRAG(ツリービュー)等が相当します。
これでExplorerへのファイルD&Dが実装できました。最初の非OLE D&DのExplorerからのファイルドロップ受付と、この実装を組み合わせればExplorerとの完全なD&Dが実現できます。
しかし、せっかくOLE D&Dの“ドラッグ”機能を実装したので、“ドロップ”機能もOLEを利用して実装することにします。

IDropTarget

では、ファイルの“ドロップ”を実現するためのIDropTargetインターフェイスを実装します。IDropTargetには次の4つのメソッドがあります。

  • IDropTarget::DragEnter()
  • IDropTarget::DragOver()
  • IDropTarget::DragLeave()
  • IDropTarget::Drop()

DragEnter()/DragOver()/DragLeave()メソッドは対象ウインドウ領域内にマウスカーソルが"入った"/"移動した"/"出た"という動きに対応して呼び出されます。これらのメソッド内でキー入力に応じてDROPEFFECTを指定することにより、IDropSourceが視覚的フィードバックを行なうめの情報を渡します。
Drop()メソッドは実際にオブジェクトが対象ウインドウにドロップされると呼び出されます。ここでIDataObjectを介して実際にデータのやり取りを行ないます。
今回はOLE D&Dのターゲットウインドウ側の(視覚的な)フィードバックは考慮していませんが、何らかのフィードバックを行いたい場合は各メソッドでリソースの初期化/破棄を行なう必要があります。

typedef struct tagOLEDropTarget {
  IDropTarget super;
  ULONG       m_cRefCnt;
} OLEDropTarget;


/* IUnknown methods */
STDMETHODIMP OLEDropTarget_QueryInterface(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_AddRef(/*略*/);
STDMETHODIMP_(ULONG) OLEDropTarget_Release(/*略*/);

/* IDropTarget methods */
STDMETHODIMP OLEDropTarget_DragEnter(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * pDataObject に応じてドロップを受け付けるかを判断
   * ドロップを拒否する際は
   *   pdwEffect = DROPEFFECT_NONE;
   * とする。
   */
  
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragOver(
  OLEDropTarget *this,
  DWORD grfKeyState,
  POINTL pt,
  DWORD *pdwEffect
  )
{
  /*
   * キー押下の状態に応じて pdwEffect をセット
   */
  return S_OK;
}

STDMETHODIMP OLEDropTarget_DragLeave(
  OLEDropTarget *this
  )
{
  return S_OK;
}

STDMETHODIMP OLEDropTarget_Drop(
  OLEDropTarget *this,
  IDataObject *pDataObject,
  DWORD grfKeyState,
  POINTL pt,D
  WORD *pdwEffect
  )
{
  /*
   * D&D完了時の処理...
   */
  return S_OK;
}

OLE D&D: Drop編

IDropTargetの準備ができたら、Explorerからのファイルドロップを受け付けるようコーディングしてみます。

RegisterDragDrop()関数を用いて、IDropTargetインターフェイスとファイルD&Dを受け付けるウインドウを関連付けます。

HWND hwnd = /* 対象ウインドウのハンドル */;
IDropTarget *lpDropTarget = /* IDropTargetのインスタンス */;

RegisterDragDrop(hwnd, lpDropTarget);

これでOLE D&Dの受け入れが登録され、ファイルがドロップされると関連付けたIDropTargetのDrop()メソッドが呼び出されるようになりました。
逆に登録を解除するにはRevokeDragDrop()関数を用いて次のようにします。

HWND hwnd = /* 対象ウインドウのハンドル */;

RevokeDragDrop(hwnd);

なおIDropTargetの参照は登録時にAddRef()/解除時にRelease()されるので明示的にRelease()する必要はありません。

OLE D&D: まとめ

これらのOLE D&Dを利用するためには、OLEライブラリの初期化を先立って行なわなければいけません。これはOleInitialize()関数で行ないます。また、利用しなくなったらOleUninitialize()関数で解放する必要があります。
これらはWinMain()で他の初期化/解放処理と一緒に行なったほうが良いでしょう。

int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,
  int nCmdShow
  )
{
  /* OLEライブラリを初期化 */
  OleInitialize(NULL);
  
  /*
   * メイン処理...
   */
  
  /* ライブラリを解放 */
  OleUninitialize();
  return 0;
}

あとがき

これで当初の目的であるExplorerとのファイルD&Dは実装できました。ただしIDataObjectの実装は手抜きですからIAdviseSinkを利用できません。これはソース側にオブジェクト(IDataObject)の変更を通知するためのインターフェイスです。また、Windows2000からはIDropSourceHelper/IDropTargetHelperというインターフェイスが追加されて、視覚的フィードバックの実装の手助けをしてくれるようです。
今回利用したOLE D&DではファイルのD&D以外にも様々なデータ(オブジェクト)のD&Dに対応しています。例えばMicrosoft Word等では選択したテキストをD&Dによってコピー&ペーストすることができますが、このようなテキストもOLE D&Dで受け取ることができます。
これらについての情報は、IDataObjectやFORMATETCといったキーワードから検索できると思います。

C++スレッド遅延開始の実装5パターン

局所的に「スレッド開始を遅延させる」ネタが盛り上がっていたので、C++とBoostライブラリを用いた色々な実装方法をまとめてみました。

この記事で対象とするのは、下記コードにある2つの要件を満たす実装方法です。

  • (1) スレッドを管理するオブジェクトXのコンストラクト時ではなく、その後の任意タイミングで新スレッド処理を開始する。
  • (2) オブジェクトXのデストラクト時に、上記(1)の別スレッドがまだ実行中ならそのスレッド処理完了を待機する。
class X {
  // threadオブジェクトを保持するメンバ変数

  void do_() { /* 別スレッド処理 */ }

public:
  ~X()
  {
    // (2) 別スレッドがまだ実行中なら完了を待機する
  }

  void start()
  {
    // (1) 新しいスレッドを開始して関数do_を実行する
  }
};

int main()
{
  X x;
  x.start();
}

実装方法の比較

#1,2,3は@cpp_akiraさんの std::threadをあとから開始。それとムーブ対応したコンテナについて - Faith and Brave - C++で遊ぼう、#4は@egtraさんのイグトランスの頭の中(のかけら) » Boost.ThreadとBoost.Optionalからのパク(ry引用です。#5が新たに追加した実装方法です。

実装方法 C++03/Boost C++11/Boost C++11/std
#1 SmartPtr+thread
#2 thread.swap
#3 threadムーブ
#4 Boost.Optimal+thread ×
#5 future+async() × ×

表中の"C++03/Boost"はC++03準拠コンパイラ+Boostライブラリ、"C++11/Boost"はC++11準拠コンパイラ+Boostライブラリ、"C++11/std"はC++11準拠コンパイラ+標準ライブラリのみの範囲において、各実装方法が実現可能/不可を示します。(なお、Boost 1.49.0を前提としています。)

注意:後述コードサンプルではC++11ラムダ式を使用しており、C++11非準拠コンパイラではコンパイルできません。C++03の範囲内では下記の書き換えが必要です。

boost::thread([this] { do_(); })
// ↓
boost::thread(&X::do_, this)

#1: SmartPtr+thread

#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>

class X {
  boost::shared_ptr<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_.reset(new boost::thread([this] { do_(); }));
  }
};

(個人的には、コピー可能なスマートポインタshared_ptrより、ムーブのみを許容するunique_ptrの方が好ましい気がします。)

#2: thread.swap

#include <boost/thread.hpp>

class X {
  boost::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    boost::thread thd([this] { do_(); });
    thread_.swap(thd);
  }
};

#3: threadムーブ

#include <thread>

class X {
  std::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    thread_ = std::thread([this] { do_(); });
  }
};

#4: Boost.Optimal+thread

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
#include <boost/thread.hpp>

class X {
  boost::optional<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_ = boost::in_place([this] { do_(); });
  }
};

#5: future+async()

この実装方法は前述のものとは異なり、threadオブジェクトの明示的な生成/保持を行いません。代わりに、launch::asyncポリシーasync関数によるスレッド開始と、futureオブジェクトのデストラクタによる(暗黙的)スレッド完了待機を行います。暗黙的なスレッドjoinについてはasync関数launch::asyncポリシーとfutureのちょっと特殊な動作 - yohhoyの日記を参照下さい。

#include <future>

class X {
  std::future<void> task_;

  void do_() { /*...*/ }

public:
  // task_がlaunch::async起動されたfutureオブジェクトの場合に限り、
  // future<void>::~future()にてスレッドjoinが暗黙的に行われる。

  void start()
  {
    task_ = std::async(std::launch::async, [this] { do_(); });
  }
};

実装#1~#4では関数do_()から例外送出されるとプログラム終了(std::terminate)しますが、実装#5は関数do_()で送出した例外を無かったこととして処理続行します。正確に表現すると、例外はちゃんとfutureオブジェクト中に格納されています。上記コードではfuture::get()による処理結果の取り出しを行っておらず、結果として関数do_()が送出した例外が失われています。

各実装はgcc 4.6.3@Ubuntu 11.10にて動作確認済みです。(なお#5はgcc 4.7.0@MacOS X 10.6だとdo_からの例外送出時のみSegmentation Faultが発生。)

Boost.Contextでファイバーライブラリを実装してみた

Boost 1.50.x候補のBoost.Contextライブラリを利用して、一風変わったファイバーライブラリを作ってみたというお話です。(注意:Boost.ContextはBoost 1.49.0の正式リリースには含まれないため、svnレポジトリのtrunkからチェックアウトする必要アリ)

注意:2012年5月現在、Boost.Contextの破壊的API変更により実装コードはコンパイルできなくなっています。id:FlastさんのBoost.Contextの怒涛の変更も参照ください。

何これ?何ができるの?

一言で表現すると「単一スレッドで動作するスレッドライブラリ」です。

これだけだと白い目で見られそうなので…もうちょい説明的な表現では「C++11標準ライブラリ相当の同期プリミティブ群を提供する、ノンプリエンティブなスレッドライブラリ」となるでしょうか。さらに協調的同期プリミティブとしてのミューテックス(mutex)、条件変数(condition variable)も併せて提供します。

例えば下記コードのように、マルチスレッドプログラムと同じ構造で記述された生産者-消費者パターンが、ファイバーライブラリ上ではシングルスレッドアプリケーションとして動作します。ちなみに、このコードでfiber名前空間→std名前空間, fiberクラス→threadクラスへと単純置換すると、そのままマルチスレッドアプリケーションになります。(サンプルコード全体はhttps://gist.github.com/2342248

// 上限付きFIFOキュー
template <class T, std::size_t N>
class bound_queue {
public:
  typedef T value_type;
  bound_queue() : q_(N) {}
  void push(value_type data)
  {
    fiber::unique_lock<fiber::mutex> lk(mtx_);
    cv_pop_.wait(lk, [=]{ return !q_.full(); });
    q_.push_back(data);
    cv_push_.notify_all();
  }
  value_type pop()
  {
    fiber::unique_lock<fiber::mutex> lk(mtx_);
    cv_push_.wait(lk, [=]{ return !q_.empty(); });
    value_type result = q_.front();
    q_.pop_front();
    cv_pop_.notify_all();
    return result;
  }
private:
  boost::circular_buffer<value_type> q_;
  fiber::mutex mtx_;
  fiber::condition_variable cv_push_;
  fiber::condition_variable cv_pop_;
};
typedef bound_queue<int, 5> QueueType;

// 生産者ファイバー
void producer(QueueType& queue)
{
  for (;;) {
    int data = /*...*/;
    queue.push(data);
  }
}

// 消費者ファイバー
void consumer(QueueType& queue)
{
  for (;;) {
    int data = queue.pop();
    //...
  }
}

前提知識

Boost.Contextはコンテキスト切り替え機能を提供するライブラリです。面倒なので(またの名を説明力不足のため)ここでは“コンテキスト切り替え”の説明は行いません。次の記事/資料などを参照してください。

本ライブラリで提供する“ファイバー(Fiber)”は、コルーチン(Coroutine)/マイクロスレッド(Micro-thread)/軽量スレッド(Lightweight thread)とも呼ばれる協調的マルチタスク機構のための部品です。通常の“スレッド(Thread)”と“ファイバー”との違いは、OSが自動的にスレッドを切り替えて並行処理を実現する(プリエンティブ・マルチタスク)か、プログラマが明示的にファイバーを切り替えて協調的に並行処理を実現する(ノンプリエンプティブ・マルチタスク)と捉えると分かり易いかと。

また、C++11標準ライブラリが提供するスレッド(std::thread)や基本的な同期プリミティブ(std::mutex, std::unique_lock, std::condition_variableなど)の使い方を理解していることを前提としています。

Pros vs. Cons

このライブラリの長所は「C++11標準スレッドライブラリと同じインターフェイスを持つので、マルチスレッド動作へシームレスに移行可能」「プログラムが常に決定的な動作となるので、本物のマルチスレッドアプリに比べてデバッグが容易」あたりでしょうか?なお、“最初からマルチスレッドで良いんじゃ?”というもっともな質問への回答は持ち合わせていません。

一方で、短所は「必ずシングルスレッドで動作するため、マルチコアといったハードウェア並行性を活用できない」「Boostライブラリのsvn-trunkに依存するのでunstable」などです。

ライブラリ設計方針

ライブラリインターフェイスは、C++11標準スレッド関連<thread>, <mutex>, <condition_variable>ヘッダ提供のAPIに可能な限り似せています。ただし、C++11標準スレッドで提供されるタイムアウト系処理(std::this_thread::sleep_for関数、std::timed_mutexクラスなど)は削除しています。これは実行ファイバーの切り替えタイミングはユーザの責任であり、タイムアウト値の指定を行ったとしてもそれを守る仕組みが存在しないためです。

ファイバー間のスケジューリング方式は、単純ラウンドロビンによる「公平(fair)スケジューリング」を採ります。つまりファイバーがブロック中か否かに関わらず、スケジューラは全ファイバーに対し平等に実行機会を与えます。例えば、ロック獲得待ちでブロック中のファイバーであっても順番がくれば起動され、ロック獲得に失敗した場合は即座に次ファイバーへと実行が移ります。

ファイバーライブラリ独自機能として、ファイバーのスケジューリングポリシ変更関数を提供します。2つのポリシ:lazyモードとeagarモードが存在し、ライブラリ規定値はlazyモードとなっています。

  • lazyモード:別ファイバーへのコンテキスト切り替えを可能な限り後回しにする。
  • eagerモード:別ファイバーへのコンテキスト切り替えを可能な限り早期に行う。

スケジューリングポリシ挙動は、新しいファイバー作成時の切り替えが分かり易いかと思います。lazyモードでは新ファイバーへのコンテキスト切り替えが“同ファイバーとのjoin時”まで遅延されますが、eagerモードでは“新ファイバー作成直後”に新ファイバーへとコンテキスト切り替えます。

fiber::fiber fb(&proc);  // eagerモード:fbに切り替え
proc();
fb.join();  // lazyモード:fbに切り替え

提供機能の一覧

C++11標準スレッドライブラリとファイバーライブラリがそれぞれ提供する機能の対応関係は下記の通りです。

C++標準スレッド ファイバーライブラリ
スレッド/ファイバー std::threadクラス
std::this_thread名前空間
fiber::fiberクラス
fiber::this_fiber名前空間
ミューテックス std::mutexクラス
std::recursive_mutexクラス
fiber::mutexクラス
fiber::recursive_mutexクラス
ロック std::lock_guardクラステンプレート
std::unique_lockクラステンプレート
fiber::lock_guardクラステンプレート
fiber::unique_lockクラステンプレート
条件変数 std::condition_variableクラス fiber::condition_variableクラス
(追加機能) fiber::get_scheduling_policy関数
fiber::set_scheduling_policy関数

ポイント解説

実行ファイバーの切り替えは、スケジューリングポリシ設定に応じて下表のコンテキスト切り替えポイントで行われます。

lazyモード eagarモード
fiberコンストラクタ
fiber::join()
mutex::lock()
recursive_mutex::lock()
mutex::unlock()
recursive_mutex::unlock()
condition_variable::wait()
condition_variable::notify_one()
condition_variable::notify_all()
this_fiber::yield()

Boost.Contextライブラリによるファイバー間コンテキスト切り替え実装は、fiber::detail::scheduler::yieldメンバ関数がコアとなります…というか本質的にこの関数が全てです。実装概略は下記の通りです。

  • fiberオブジェクトとコンテキストboost::contexts::contextとを1:1対応させます。なお、プログラムに最初から1つあるファイバーは、便宜上“メインファイバー”と呼びます。
  • ファイバーは 実行状態/コンテキスト未開始状態/yieldメンバ関数内で休止状態 の何れかの状態を取ります。なお、スケジューラから見た未開始状態と休止状態の違いは、コンテキストに対して次に呼ぶべきメソッドがstartかresumeかの差異だけです。また常にただ1個のファイバーが実行状態にあり、プログラム開始直後はメインファイバーがそれに該当します。
  • コンテキスト切り替えでは必ずメインファイバーのコンテキストを経由します。すなわち、yieldメンバ関数がメインファイバー以外のコンテキストで呼び出された場合は、suspendで一旦メインファイバーへコンテキストを戻します。続いてスケジューラが決定した次ファイバーのコンテキストをstart/resumeし、コンテキスト切り替えの後にyieldメンバ関数を抜けます。ちなみに次ファイバーがメインファイバーの場合は、コンテキストのresumeを行わずにyieldメンバ関数を抜けるだけです。

ライブラリコード

ライブラリコードはBoost Software License 1.0で公開しています。動作確認はGCC 4.6.3+Boost svn-trunk rr77773@Ubuntu 11.10で行いました。[https://gist.github.com/2318086

既知の問題とか

このライブラリはコンセプト実証レベルで実装しているため、下記箇所で手抜きをしています。

  • fiberコンストラクタ引数でコピー不可かつムーブ可能な型を扱えない(std::threadはできる)
  • fiber::id型が単にuintptr_tのtypedef(std::thread::idは独立したclass)
  • C++11標準ライブラリ<future>の提供APIに未対応(std::future, std::asyncなど)