先日、C++でマルチエージェントシステムを書いているときに、ん?となったので記事にしました。
ソースコード
問題となったコードは以下のようなものです。
std::weak_ptr<sample> self = agent;
std::thread t([self]() {
for(auto s = self.lock(); s && s->predicate(); s = self.lock()) {
//do something
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
t.detach();
非同期にエージェントを一定間隔で更新して、スレッドの外では例えばエージェントの状態を表示する、というようなコードです。
問題
このままではスレッド外でagentの所有権が手放された場合メモリリークを起こす可能性があります。問題の部分がfor文の更新式です。
具体的に説明する前にまずはstd::weak_ptr::lockの定義を確認しましょう。
shared_ptr<T> lock() const noexcept;
https://cpprefjp.github.io/reference/memory/weak_ptr/lock.html より引用
上記の通りlock()が返す値はstd::shared_ptr<T>の一時オブジェクトです。つまりlock()で作られたオブジェクトの寿命は式の評価が終了するまで、ということです。shared_ptrへの代入では自身の持つリソースを放棄してから代入しますが、その間にも別のshared_ptrがそのリソースを保持しているため破棄されずに残ってしまいます。
解決策
解決策は所有しているオブジェクトを使った後すぐに放棄してしまうことです。
コードは以下のようになります。
std::weak_ptr<sample> self = agent;
std::thread t([self]() {
for(auto s = self.lock(); s && s->predicate(); s = self.lock()) {
//do something
s.reset(); //abandon
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
t.detach();
s.reset()はsleepの前後どちらでも動作しますが、後ろに置いた場合不必要にオブジェクトの寿命が延ばされることになるので、前に置いた方がよいと思います。
というわけで、マルチスレッド環境で遭遇したshared_ptrの注意点でした。