新聞を読もう! その多様性と偏向を意識して · 新聞を読もう。新聞を読むことは、最も手軽にでき る読書だ。だが、どうせ読むのなら、大学生らしい読
規格書で読むC++11のスレッド
-
Upload
kohsuke-yuasa -
Category
Technology
-
view
5.835 -
download
3
description
Transcript of 規格書で読むC++11のスレッド
2013/07/13
規格書で読むC++11のスレッド
@hotwatermorning
1
発表者自己紹介✤ @hotwatermorning✤ DTMer✤ C++ポケットリファレンス書きました!(共著)✤ Amazonのなか見検索に対応!
2
http://www.amazon.co.jp/C-%E3%83%9D%E3%82%B1%E3%83%83%E3%83%88%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9-%E9%AB%98%E6%A9%8B-%E6%99%B6/dp/4774157155
本日のセッションを始める前に
✤今日のセッションはC++の規格と絡むので、手元に規格書があると便利です。
✤本日は規格書の代わりに、N3337を使用します。✤ ※PDF,割とサイズが大きいので注意
3
C++のスレッド
4
Free Lunch Is Over
5
✤タダ飯の時代は終わった✤ 2005年マイクロソフトの Software Architect である Herb Sutterの言葉
✤ CPUコアのクロック数向上が頭打ちとなり、時を待てば自ずとソフトウェアの性能が上がるという時代の終わり
✤→並行プログラミングが重要に
C++のスレッド
✤ 2011年に策定された新規格(通称:C++11)から、C++にマルチスレッドサポートが導入された。
✤これによって、ポータブルなコードでマルチスレッドアプリケーションをかけるようになった。
6
#include <iostream>#include <thread>#include <vector>#include <functional>
int main() { std::vector<int> data = GetSomeData(); int sum;
std::thread th( [](std::vector<int> const &data_, int &sum_) { sum_ = 0; for(auto n : data_) { sum_ += n; } }, std::cref(data), std::ref(sum) );
th.join();
std::cout << "sum of data is : " << sum << std::endl;}
C++のスレッド
7
C++03との違い
8
C++03までのスレッド
9
✤規格で、マルチスレッドをサポートしたメモリモデルや実行スレッドが定義されていなかった。
✤そのため、これまでC++でマルチスレッド処理をするためには、各プロセッサーのメモリモデルや各処理系のスレッドの定義に依存したコードを書く必要があった。
C++11からのスレッド
10
✤規格で、マルチスレッドをサポートしたメモリモデルや実行スレッドが定義された。
✤マルチスレッドための言語機能やライブラリも導入された。
✤標準機能のポータブルなコードでマルチスレッドが実現できるようになった。
✤スレッドライブラリも、最新のC++の知見をふんだんに使用しているので、洗練されている。
ライブラリ
11
✤ thread✤ mutex/lock✤ future/promise✤ async/packaged_task✤ condition_variable✤ call_once✤ atomic
言語機能
12
✤ static変数の初期化✤ thread_local変数のサポート✤スレッドを超えた例外の伝播(std::exception_ptr)✤などなど
本日やる内容
13
本日やる内容
14
✤ thread✤ mutex/lock✤ future/promise✤ condition_variable
本日やる内容
15
✤ thread✤ mutex/lock✤ future/promise✤ condition_variable
thread
✤スレッドを扱うクラス (§30.3.1)✤ “threadクラスは新たな実行スレッドを作成したり、そのスレッドの終了を待機したり、その他スレッドの状態を問い合せる操作のメカニズムを提供します。”
✤スレッドの作成やスレッドハンドルの管理を行う✤ そのためプログラマが常に明示的にスレッドの作成やハンドルの管理を意識する必要がない
✤ Not Copyable / Movable
16
スレッドの作成
✤ threadクラスのコンストラクタで、第一引数に関数や関数オブジェクトを渡し、第二引数以降にその関数に適用させたい引数を渡す(§30.3.1.2/3)(§20.8.2/1)
17
#include <iostream>#include <thread>
void foo(){ std::cout << "Hello" << std::endl;}
int main() { // 別スレッドでfoo関数を起動 std::thread th(foo);
// スレッドの終了を待機 th.join();}
スレッドの作成
18
template <class F, class ...Args>explicit thread(F&& f, Args&&... args);// というコンストラクタについて、
INVOKE ( DECAY_COPY( std::forward<F>(f) ), DECAY_COPY( std::forward<Args>(args) )...)// が有効であるような関数や引数を渡せる
コンストラクタの定義
19
DECAY_COPY()
✤引数に渡されたオブジェクトをコピーして受け渡す操作を表す。(§30.2.6)
✤ただし、rvalueなオブジェクトはそのままムーブされたり、配列は先頭要素へのポインタにするなどの変換が行われる。
✤詳しくはhttp://d.hatena.ne.jp/yohhoy/20120306/p1
20
INVOKE()
✤関数やメンバ関数へのポインタや関数オブジェクト(Functor)やラムダ式など、なんらか呼び出し可能なものを第1引数に取り、続く引数を適用して呼び出す操作を表す。(§20.8.2)
✤ https://sites.google.com/site/cpprefjp/reference/functional/invoke
✤ http://twitter.com/Cryolite/status/216814363221303296
21
// INVOKEが表す仮想的な呼び出し操作はINVOKE(f, t1, t2, ..., tN);
// 実際には以下から適切なものが呼び出される(t1.*f)(t2, ..., tN);
((*t1).*f)(t2, ..., tN);
f(t1, t2, ..., tN);
INVOKE()
22
struct Foo { // operator()をもつクラス // → 関数オブジェクト(別名:ファンクタ) void operator() (std::string msg) const { std::cout << "Hello " << msg << std::endl; }};
Foo foo;std::string msg = "World!";
foo("World"); // Hello World!
//関数のように呼び出せる
INVOKE()
23
int main() { std::string msg = "World!"; Foo foo;
// 関数のように呼び出せるものは // スレッドに渡せる std::thread th(foo, msg); // Hello World!
//スレッドの終了を待機 th.join();}
INVOKE()
24
スレッドに参照を渡す
✤スレッドの引数にオブジェクトの参照を渡そうとすると、DECAY_COPY()によって、コンパイルエラーとなる。✤ 参照型はコピーもムーブもできないため
✤そのため、DECAY_COPY()に渡す前に参照をstd::ref()やstd::cref()で包んで渡すようにする。
✤ http://d.hatena.ne.jp/yohhoy/20120306/p1
25
#include <thread>#include <iostream>#include <functional>
void add(int a, int b, int &c) { c = a + b;}
int main() { int result; std::thread th1(add, 1, 2, std::ref(result));
th1.join(); std::cout << result << std::endl;}
スレッドに参照を渡す
26
thread
✤スレッドを扱うクラス (§30.3.1)✤ “threadクラスは新たな実行スレッドを作成したり、そのスレッドの終了を待機したり、その他スレッドの状態を問い合せる操作のメカニズムを提供します。”
✤スレッドの作成やスレッドハンドルの管理を行う✤ そのためプログラマが常に明示的にスレッドの作成やハンドルの管理を意識する必要がない
✤ Not Copyable / Movable
27
Copyable Type
✤ CopyConstructible要件 とCopyAssignable要件 を満たす型 (§17.6.3.1)
✤データをコピーして、オブジェクト間で同じ状態を実現できる
✤(Copyable自体はC++規格に定義された用語ではないが、上の2つの性質をまとめてCopyableという)
28
// CopyConstructibleT u = v;T(v); // CopyAssignableu = v;
Copyable Type
29
Movable Type
✤ MoveConstructible要件 とMoveAssignable要件 を満たす型 (§17.6.3.1)
✤データ(の所有権)を移動して、オブジェクト間でデータを受け渡せる
✤(Movable自体はC++規格に定義された用語ではないが、上の2つの性質をまとめてMovableという)
30
// MoveConstructibleT u = SomeFunctionReturnsT();T(SomeFunctionReturnsT()); // MoveAssignableu = SomeFunctionReturnsT();
Movable Type
31
// MoveConstructibleT u = std::move(t1);T(std::move(t2)); // MoveAssignableu = std::move(t3);
Movable Type
32
#include <cassert>#include <thread>#include <iostream>
void foo(int a, int b) { std::cout << a << ", " << b << std::endl; }
int main() { std::thread th1(foo, 1, 2);
// コンパイルエラー std::thread th2 = th1; // ムーブなら問題ない std::thread th3 = std::move(th1); // ムーブ元のth1はスレッドを手放している。 assert(th1.get_id() == std::thread().get_id());
th3.join();}
スレッドの受け渡し
33
thread
✤ threadクラスのデストラクタと、スレッド管理の問題
34
デストラクタとjoin/detach
35
✤ thread::join()はスレッドの終了を待機し、threadオブジェクトを初期化する関数
✤ thread::detach()はスレッドの管理を手放し、threadオブジェクトを初期化する関数
デストラクタとjoin/detach
36
✤なんらかのスレッドを管理しているthreadオブジェクトが、join()もdetach()も呼び出さずにデストラクトされると、std::terminate()を呼び出して、プログラムが強制終了する。✤ デストラクタで自動的にjoin()やdetach()を呼び出すことにすると、プログラマの意図しないjoin()やdetach()の呼び出しが発生し、予期しないバグを引き起こす可能性があるため。
✤ http://d.hatena.ne.jp/yohhoy/20120209/p1✤ http://d.hatena.ne.jp/yohhoy/20120211/p1✤ http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2802.html
void worker(SomeParam p){ doSomething();}
void foo(SomeParam p){ std::thread th(worker, p); // このまま関数を抜ける // →std::terminate()で強制終了}
デストラクタとjoin/detach
37
デストラクタとjoin/detach
38
✤このままでは、あまりに活用しにくいように思われるが、そもそもthreadクラスはC++でスレッドを扱うための最もプリミティブな機能なので、このようになっている。
デストラクタとjoin/detach
39
✤マルチスレッドや非同期処理のより高級な仕組みとしてstd::asyncやstd::future/std::promiseなどが用意されている。
✤ただしマルチスレッドプログラミングに便利な機能が充分に揃っているとは言えない。
✤なのでそういうのが必要な場合はTBBや Microsoft PPLなどを使ったほうがいいかも?✤ http://corensic.wordpress.com/2011/10/10/async-tasks-in-c11-not-quite-there-yet/
✤ http://d.hatena.ne.jp/yohhoy/20120417/p1
デストラクタとjoin/detach
40
✤明示的にjoin()/detach()しなければプログラムが強制終了する挙動は、そのままでは例外機構との相性が悪い。
✤ RAIIイディオムを用いて、自動でjoinを行う方法などが以下に紹介されている。✤ http://akrzemi1.wordpress.com/2012/11/14/not-using-stdthread/✤ http://www.boost.org/doc/html/thread/ScopedThreads.html
本日やる内容
41
✤ thread✤mutex/lock✤ future/promise✤ condition_variable
Mutex/Lock
42
✤排他処理を実現する仕組み
Mutexクラス
43
✤スレッド間で排他的に所有されるリソースを表すクラス (§30.4)✤ “ミューテックスオブジェクトは、データ競合に対する保護を容易にし、execution agents間での安全なデータ同期を可能にします。”
Mutexクラス
44
✤標準で定義されているMutexクラスは以下の4つ✤ std::mutex✤ std::recursive_mutex✤ std::timed_mutex✤ std::recursive_timed_mutex
std::mutex
45
✤再帰的ロック不可なミューテックス (§30.4.1.2.1)✤ Lockable
std::recursive_mutex
46
✤再帰的ロック可能なミューテックス (§30.4.1.2.2)✤ Lockable✤Window APIのMutexやCritical Sectionの挙動と同じ
Lockable Type
47
✤排他処理のために、標準規格のスレッドライブラリから使用されるオブジェクトに必要とされる要件を満たす型 (§30.2.5.3)
✤以下の3つのメンバ関数を持つなど✤ m.lock()✤ m.try_lock()✤ m.unlock()
SomeLockableType m;
// 所有権を取得m.lock();
// 所有権の取得を試行m.try_lock();
// 所有権を手放すm.unlock();
Lockable Type
48
std::timed_mutex
49
✤再帰的ロック不可な時間制限機能付きミューテックス
✤ロック処理を制限時間まで試行する✤ TimedLockable
std::recursive_timed_mutex
50
✤再帰的ロック可能な時間制限機能付きミューテックス
✤ロック処理を制限時間まで試行する✤ TimedLockable
TimedLockable Type
51
✤排他処理のために、標準規格のスレッドライブラリから使用されるオブジェクトに必要とされる要件を満たす型 (§30.2.5.3)
✤ LockableTypeの性質に加え、以下のメンバ関数を持つなど✤ m.try_lock_for()✤ m.try_lock_until()
#include <chrono>
SomeTimedLockableType m;
// 指定時間だけ所有権の取得を試行m.try_lock_for( std::chrono::seconds(3) );
// 指定時刻まで所有権の取得を試行m.try_lock_until( std::chrono::steady_clock()::now() + std::chrono::seconds(3) );
Lockable Type
52
Lockクラス
53
✤ Lockableなオブジェクトを管理するRAIIクラス (§30.4.2)✤ ““lock”とはLockableなオブジェクトの参照を保持し、スコープを抜けるなどのデストラクト時には、そのLockableなオブジェクトをunlockするようなオブジェクトです。execution agentはlockableオブジェクト(のロック)の所有権を、例外安全のマナーに則って管理するための助けとして、この"lock"を使用できます。”
Lockクラス
54
✤それ自身は、排他処理のための直接的な機能は持たずに、参照として保持するMutexクラスを管理するだけ。
Lockクラス
55
✤標準規格で定義されているLockクラス✤ std::lock_guard<Mutex>✤ std::unique_lock<Mutex>
std::lock_guard<Mutex>
56
✤ミューテックスのシンプルな管理機構を実現するクラステンプレート
✤テンプレート引数Mutexは、BasicLockableでなければならない
BasicLockable Type
57
✤排他処理のために、標準規格のスレッドライブラリから使用されるオブジェクトに必要とされる要件を満たす型 (§30.2.5.3)
✤以下の2つのメンバ関数を持つなど✤ m.lock()✤ m.unlock()
✤前述のLockable Typeは、BasicLockable Typeの性質を含んでいる
SomeBasicLockableType m;
// 所有権を取得m.lock();
// 所有権を手放すm.unlock();
BasicLockable Type
58
struct Worker { //... void process() { for(int i = 0; i < 100; ++i) { //このスコープ内だけ排他処理する std::lock_guard<std::mutex> lock(mutex_); data_ = doSomething(data_); } }private: std::mutex mutex_; int data_; // ...};
std::lock_guard<Mutex>
59
Lockクラス
60
✤標準規格で定義されているLockクラス✤ std::lock_guard<Mutex>✤ std::unique_lock<Mutex>
std::unique_lock<Mutex>
61
✤ lock_guardよりも高級な処理ができるクラス✤ Mutexオブジェクトの再割当て、ムーブ、try_lock_*によるロックの試行
Lockクラス
62
✤ Lockクラスによって、プログラマが明示的にlock()/unlock()を対応付けて管理する必要がなくなる。
✤ RAIIというイディオムによって、例外安全性も高まる。✤ http://d.hatena.ne.jp/heisseswasser/20130508/1367988794
本日やる内容
63
✤ thread✤ mutex/lock✤ future/promise✤ condition_variable
future/promise
64
✤並行プログラミングのPromise/Futureパターンを実現する。
✤あるスレッドから、同じあるいは異なるスレッドで走る関数の結果を受け取るためのコンポーネント。マルチスレッドプログラミングのためだけではなく、シングルスレッドプログラムでも、非同期処理に役立つ。(§30.6.1)
future/promise
65
✤ std::promiseクラスとstd::futureクラスの対応するオブジェクト同士は、一つのshared stateを共有する。(§30.6.4)
✤データを作る側はpromiseに値を設定し、データを受け取る側ではfutureから値を取得する
void sum_async( std::vector<int> const &data, std::promise<int> promise) { int sum = 0; for(auto n : data) { sum += n; } promise.set_value(sum); // promiseにデータをセットし}
int main() { std::vector<int> data = { 1, 2, 3 }; std::promise<int> p; std::future<int> f = p.get_future();
std::thread th(sum_async, std::cref(data), std::move(p));
th.detach(); std::cout << f.get() << std::endl; // futureで受け取る}
future/promise
66
std::promise<R>
67
✤ set_value(R const &)✤ 非同期処理の結果として値を設定
✤ この形式の他に、Rの型によって異なるシグネチャのものが存在する。
✤ set_exception(std::exception_ptr)✤ 非同期処理の結果として例外を設定
std::future<R>
68
✤ get()✤ 非同期処理の結果を取得する
✤ この形式の他に、Rの型によって戻り値やシグネチャが異なるものが存在する。✤ promiseで例外が設定された場合は、get()の呼び出しで、もとの例外が再送出される
✤ wait()✤ 非同期処理の結果がpromiseに設定されるまで処理をブロックする(値/例外はまだ取得しない)
void sum_async( std::vector<int> const &data, std::promise<int> promise) {
if(data.empty()) { std::exception_ptr pe = std::make_exception_ptr( UnexpectedDataLengthError() ); // エラーを表すときは、promiseに例外をセットする promise.set_exception(pe);
} else { int sum = 0; for(auto n : data) { sum += n; } promise.set_value(sum);
}}
例外の受け渡し(セット側)
69
int main() { std::vector<int> data = {}; std::promise<int> p; std::future<int> f = p.get_future();
std::thread th( sum_async, std::cref(data), std::move(p)); th.detach();
try { std::cout << f.get() << std::endl;
// futureのget()で例外が再送出される } catch(UnexpectedDataLengthError &e) { std::cout << e.what() << std::endl; }}
例外の受け渡し(取得側)
70
本日やる内容
71
✤ thread✤ mutex/lock✤ future/promise✤ condition_variable
condition_variable
72
✤条件変数という、スレッド間で同期をとる仕組み (§30.5/1)✤ “条件変数は、ある条件が満たされた事によって他のスレッドから通知を受けたり、システム時間が設定時刻に到達するまでスレッドをブロックするのに使用される同期プリミティブを提供します。”
条件変数の動作原理
73
✤あるスレッドでミューテックスにロックをかけ、条件変数にミューテックスを渡す。
✤条件変数はミューテックスのロックを解放し、処理をブロック、スレッドは待機状態になる。
✤別のスレッドから待機状態のスレッドに起動通知を送る
✤条件変数はロックを再取得し、待機状態のスレッドを再開させる
std::condition_variable cond;std::mutex mutex;bool data_ready;
void process_data();
void wait_for_data_to_process() { std::unique_lock<std::mutex> lock(mut); // データが準備できるまで待機する。 while(!data_ready) { cond.wait(lock); } process_data();}
condition_variable
74
void retrieve_data();void prepare_data();
void prepare_data_for_processing() {
retrieve_data(); prepare_data();
{ boost::lock_guard<boost::mutex> lock(mut); data_ready=true; } // データが準備できたら待機状態のスレッドを起こす cond.notify_one();}
condition_variable
75
ミューテックスの型
76
✤ std::condition_variableではミューテックスにstd::unique_lock<std::mutex>を使用する。
✤ std::condition_variable_anyでは、排他処理に、std::unique_lock<std::mutex>以外の別のクラスを使用できる。
hwm.task
77
ここまでの機能
78
✤ thread✤ mutex/lock✤ future/promise✤ condition_variable✤これらを使って、タスクライブラリっぽいものを作ってみる。
hwm.task
79
✤ Github : https://github.com/hotwatermorning/hwm.task.git
hwm.task
80
✤ Github : https://github.com/hotwatermorning/hwm.task.git✤ git cloneして持ってきて、SConstructに指定しているBoostのパスなどを適宜書き換えて、$> sconsするとサンプルコードがビルドできる。(※scons必要)
✤ gcc 4.8で動作確認済み。
hwm.task
81
✤概要✤ いくつかスレッドを起動させ、タスクキューにタスクが追加されると、どれかのスレッドがそれを処理する。
✤ 起動するスレッドの数はキューの初期化時に設定できる✤ タスクはINVOKE()のように呼び出せるものであればなんでもいい。
✤ タスクキューへのタスクの追加はスレッドセーフ✤ タスクキューから自動でタスクが取り出され実行され、その実行結果は、std::futureを通じて非同期に取得できる。
hwm.task
82
✤ task_queue.hpp✤ タスクキュー本体。タスクを管理し、適宜どれかのスレッドで取り出して実行する
hwm.task
83
✤ locked_queue.hpp✤ タスクキューの実装に使用している、コンテナ。✤ Producer/Consumerパターンを使用したマルチスレッドセーフなキュー
hwm.task
84
✤ task_base.hpp/task_impl.hpp✤ タスクを表すクラス。✤ run()メンバ関数によって実行される。✤ 実際の処理を行う部分はType Erasureというイディオムによって、baseクラスに隠蔽される。
//! タスクキューで扱うタスクを表すベースクラス
namespace hwm {namespace detail { namespace ns_task {
struct task_base{ virtual ̃task_base() {} virtual void run() = 0;};
}}}
task_base
85
//! タスクの実体クラス//! Boost.Preprocessorを用いて、10引数を取るタスクまでをサポートtemplate<class F BOOST_PP_ENUM_TRAILING(11, HWM_TASK_template_parameters, unused)>struct task_impl;
#define BOOST_PP_LOCAL_MACRO(iteration_value) \ template<class F BOOST_PP_ENUM_TRAILING(iteration_value, HWM_TASK_template_parameters_specialized, unused)>\ struct task_impl<F BOOST_PP_ENUM_TRAILING_PARAMS(iteration_value, Arg) /*BOOST_PP_ENUM_TRAILING(BOOST_PP_SUB(10, iteration_value), HWM_TASK_default_params, unused) */>\ : task_base \ {\ typedef typename function_result_type<F BOOST_PP_ENUM_TRAILING_PARAMS(iteration_value, Arg)>::type result_type;\ typedef std::promise<result_type> promise_type;\ task_impl(promise_type && promise, F && f BOOST_PP_ENUM_TRAILING_BINARY_PARAMS(iteration_value, Arg, &&arg))\ : promise_(boost::move(promise))\ , f_(std::forward<F>(f))\ BOOST_PP_ENUM_TRAILING(iteration_value, HWM_TASK_initialize_member_variables, unused)\ {}\ private:\ task_impl(task_impl const &) = delete;\ task_impl & operator=(task_impl const &) = delete;\ promise_type promise_;\ F f_;\ BOOST_PP_REPEAT(iteration_value, HWM_TASK_define_member_variables, unused)\ virtual\ void run() override final\ {\ try {\ promise_.set_value(f_(BOOST_PP_ENUM(iteration_value, HWM_TASK_apply_member_variables, unused)));\ } catch(...) {\ promise_.set_exception(std::current_exception());\ }\ }\ };\ /**/
task_impl
86
//! タスクの実体クラス//! Boost.Preprocessorを用いて、10引数を取るタスクまでをサポートtemplate<class F>struct task_impl { task_imp(std::promise<F()の戻り値> &&promise, F &&f) : promise_(std::move(promise)) , f_(std::forward<F>(f)) {}
std::promise<F()の戻り値> promise_; F f_;
void run() override final { try { promise_.set_value(f_()); } catch(...) { promise_.set_exception(std::current_exception()); } }};
task_impl
87
//! タスクの実体クラス//! Boost.Preprocessorを用いて、10引数を取るタスクまでをサポートtemplate<class F, class Arg0>struct task_impl { task_imp(std::promise<F()の戻り値> &&promise, F &&f, Arg0 &&arg0) : promise_(std::move(promise)) , f_(std::forward<F>(f)) , arg0_(arg0) {} std::promise<F()の戻り値> promise_; F f_; Arg0 arg0_; void run() override final { try { promise_.set_value(f_(std::forward<Arg0>(arg0_))); } catch(...) { promise_.set_exception(std::current_exception()); } }};
task_impl
88
example
89
namespace hwm {
struct task_queue {
// タスクをキューに追加する template<class F, class... Args> std::future<F(Args)の戻り値の型> > enqueue_sync(F f, Args... args); //...};
}
タスクの追加
90
hwm::task_queue tq(スレッド数);
std::future<int> f = tq.enqueue_sync( [](int x1, int x2) -> int { std::cout << (x1 + x2) << std::endl; return x1 + x2; }, 10, 20 ); // タスクキューによって自動的に非同期に実行される
タスクの追加
91
// タスクキューに積まれた処理の結果は、// タスク追加時に返されたfutureオブジェクトを使用して// 取得するstd::cout << "calculated value : " << f.get() << std::end;
実行結果の取得
92
まとめ
✤ C++11から、言語規格でスレッドがサポートされました。
✤スレッドを扱うためのライブラリも言語に追加されました。
✤これでマルチスレッドライブラリ/アプリケーションを書くのが容易になります。
93
まとめ
✤並行プログラミングを極めてタダ飯の時代を!
94