view task_manager.tex @ 3:3ee6deaab278

*** empty log message ***
author gongo
date Mon, 14 Jul 2008 16:28:37 +0900
parents d40dd97c0a50
children 6458878b4526
line wrap: on
line source

\section{Task Manager} \label{sec:tm}
Task Manager は、Task と呼ばれる分割された各プログラムを管理する。
Task の単位はサブルーチンまたは関数とし、Task 同士の依存関係を考慮しながら
実行していく。

現在実装されている Task Manager の API を \tabref{tab:tm-api} に示す。

\begin{table}[htbp]
  \caption{Task Manager API} \label{tab:tm-api}
  \hbox to\hsize{\hfil
  \begin{tabular}{c|l} \hline \hline
    create\_task & Task を生成する \\ \hline
    run          & 実行 Task Queue の実行 \\ \hline
    allocate     & 環境のアライメントを考慮した allocater \\ \hline
    \hline
    add\_inData   & Task への入力データのアドレスを追加 \\ \hline
    add\_outData  & Task からのデータ出力先アドレスを追加 \\ \hline
    add\_param    & Task のパラメータ (32 bits) \\ \hline
    wait\_for    & Task の依存関係の考慮 \\\hline
    set\_cpu     & Task を実行する CPU の設定 \\ \hline
    spawn        & Task を実行 Task Queue に登録する \\ \hline
  \end{tabular}\hfil}
\end{table}

以下に Task Manager を使った記述例を示す。

{\small
\begin{verbatim}

char sendStr[STRSIZE] = "Hello, World";

int
main(void)
{
    TaskManager *manager;
    Task *task;
    int length = sizeof(char)*STRSIZE;

    /**
     * Constructor
     * @param CPU_NUM: 使用する CPU の数
     */
    manager = new TaskManager(CPUNUM);

    // TASK_RUN = TaskRun::run に対応する ID
    task = create_task(TASK_RUN);

    /**
     * タスクの入出力データの設定
     * @param[1]: address of data
     * @param[2]: size of data
     */
    task->add_inData(sendStr,length);
    task->add_outData(sendStr,length);
    
    task->spawn();

    printf("before: %s\n", sendStr);

    manager->run();

    printf("after : %s\n", sendStr);

    return 0;
}

TaskRun::run(void *rbuf, void *wbuf)
{
    // add_inData で指定したアドレスのデータを取得
    // recvStr = "Hello, World"
    char *recvStr = (char*)get_input(rbuf, 0);

    // fixStr にあるデータが、
    // Task 終了後、add_outData で指定した
    // sendStr に書き込まれる
    char *fixStr = (char*)get_input(wbuf, 0);

    strcpy(fixStr, recvStr);
    fixStr[0] = 'D';
    fixStr[3] = 'E';
}

// 実行結果
before: Hello, World
after: DelEo, World

\end{verbatim}
}

\subsection{Task の定義} \label{sec:tm-task}
タスクの定義を以下に示す。

{\small
\begin{verbatim}

class Task {
public:
    int command;
    ListData inListData;  // list of input data
    ListData outListData; // list of output area
    Task* self;
    int param_size;
    int param[3];
};

class HTask : public Task {
public:
    // List of task waiting for me
    TaskQueuePtr wait_me;
    // List of task for which I am waiting
    TaskQueuePtr wait_i;

    CPU_TYPE cpu_type;
};

\end{verbatim}
}

Task クラスは、各 Core が実行するタスクの単位オブジェクトである。
各 Core は inListData にあるアドレス(メインメモリ)からデータを取得し、
command に対応するコードを実行し、結果を outListData にあるアドレスに送信する。
これらの処理はパイプラインに沿って動作する (sec:\ref{sec:tm-scheduler})。
各 Core には Task の集合である TaskList を送る。

HTas クラスは、TaskManager で管理する実行前のタスクオブジェクトである。
wait\_me, wait\_i はタスク依存の条件 (sec:\ref{sec:tm-depend}) に、
cpu\_type は実行する CPU の切り替え (sec:\ref{sec:tm-cpu}) に用いる。

\subsection{スケジューラ} \label{sec:tm-scheduler}
TaskManager のスケジューラを以下に示す。

\begin{verbatim}

SchedTaskBase *task1, *task2, *task3;

do {
    task3->write();
    task2->exec();
    task1->read();

    taskTmp = task3;
    task3 = task2;
    task2 = task1;
    task1 = task1->next(taskTmp);
} while (task1);

\end{verbatim}

TaskList にある Task が全て終了し、メインスレッドから
終了のメッセージを受け取ったら、while 文を抜ける。

SchedTaskBase クラスは、スケジューラによって実行されるオブジェクトである。
スケジューラ自身のタスクとして、以下のようなタスクがある。
これらのオブジェクトは全て SchedTaskBase を継承している。

\begin{itemize}
\item SchedMail: メインスレッドからのメッセージを取得する
\item SchedTaskList: TaskList を取得する
\item SchedTask: Task を実行する
\item SchedExit: Core の実行を終了する
\item SchedNop: 何も行わない
\end{itemize}

ユーザがタスクを記述する場合、SchedTask を継承し、exec() 内の run() に
タスクの処理を記述する。

\subsection{タスク依存} \label{sec:tm-depend}
Task Manager はタスク依存を解決する機能を持っている。
以下は記述例である。

\begin{verbatim}

// task2 は task1 が終了してから開始する
task2->wait_for(task1);

\end{verbatim}

タスク依存が満たされたタスクを ActiveQueueに入れる。
各 Core は ActiveQueue から処理するコードとデータを取得し、
自律的に実行する。終了したタスクはメインスレッドへ終了のコマンドを
発行紙、メインスレッドはそれを見て WaitQueue のタスクを調べ、
タスク依存を満たしたタスクを ActiveQueue に入れる。

\subsection{タスクを実行させる Core の選択} \label{sec:tm-cpu}
TaskManager の \verb|set_cpu()| により、タスクを
どの Core で実行するか選択する事が出来る。

\begin{verbatim}

// Core 1 で実行する
task->set_cpu(CPU_1);

// どの Core で実行してもいい
task->set_cpu(CPU_ANY);

\end{verbatim}

メインスレッド内でもタスクを実行する事が可能なため、これを用いる事により、
環境依存によるプログラム変換はタスクの部分だけとなり、全体の変換は必要ない。

このことはデバッグにおいても有効であると言える。
デバッグが困難な並列プログラムの前に、まずはメインスレッド単体で動く
シーケンシャルプログラムを記述する。
仕様やアルゴリズムの正しさを確認できたら、\verb|set_cpu| により
各 Core へタスクを渡し、並列プログラムへ移行する。
デバッグに関する詳細は第 \ref{sec:debug} 節で述べる。

\subsection{メインスレッドと各 Core 間との同期}
メインスレッドと各 Core 間では、32 ビットメッセージの
交換により同期を行っている。これは Cell の機能の一つである、
Mailbox を元に実装した。

Mailbox とは SPE の MFC 内の FIFO キューであり、
PPE と SPE 間の 32 ビットメッセージの交換に用いられる \cite{mailbox} 。
通常、スレッド間で待ち合わせを行うと処理が止まってしまい、
並列度が下がってしまうことがあるが、Mailbox は
メッセージ交換なので待ち合わせを避けることが可能である。

\figref{fig:sync} は、Cerium における
PPE-SPE 間のメッセージのやりとりを表している。
メインスレッドでは、各 SPE の起動と終了、そしてタスクの管理を行っている。
そして Outbound Mailbox (SPE $\rightarrow$ PPE メッセージ) を見て、
その内容に対応する処理を PPE 上で行う。
対処した結果を Inbound Mailbox (PPE $\rightarrow$ SPE メッセージ) で伝え、
受け取った SPE はタスクを再実行する。

\begin{figure}[tb]
  \begin{center}
    \includegraphics[scale=0.353]{figure/sync.pdf}
    \caption{PPE, SPE threads}
    \label{fig:sync}
  \end{center}
\end{figure}