質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

2回答

3797閲覧

unique_lockとunique_ptrの順序によってメモリ解放漏れが発生する

HARQ

総合スコア181

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

1クリップ

投稿2018/02/08 12:45

編集2018/02/08 12:52

概要

unique_ptrを要素とするdequeを、スレッド間のデータ受け渡しに使おうとしています。
排他制御にはmutex(lock_guardおよびunique_lock)とcondition_variableを使用しています。
が、受け取りスレッド側におけるunique_lockとunique_ptrの位置関係によって、メモリが解放されていないように見える現象に悩まされています。
原因・解決策について知見をお持ちの方がいらっしゃいましたら、ご回答よろしくお願いいたします。

環境

  • Visual Studio Professional 2017
  • Visual C++ / Windows コンソール アプリケーション

再現コード

コード中、NGとコメントしてある箇所でunique_ptrの定義を行うと、メモリ使用量が増加し続けます(タスクマネージャーで確認、送信側でbad_alloc発生)。
本来 NG その1の箇所で定義・解放を行いたいのですが、NG その2の箇所で定義しても同様となります。
しかしOKとコメントしてある箇所で定義した場合は、そのような現象が発生しません。

C++

1#include "stdafx.h" 2#include <cstdint> 3#include <mutex> 4#include <condition_variable> 5#include <deque> 6#include <thread> 7#include <iostream> 8 9using namespace std; 10 11static constexpr auto LENGTH = 2 * 1024 * 1024; 12static mutex mtx; 13static condition_variable cond; 14static deque<unique_ptr<uint8_t[]>> que; 15 16void reader() 17{ 18 while (true) { 19 unique_ptr<uint8_t[]> values; // NG その1 20 21 // Critical section. 22 { 23 //unique_ptr<uint8_t[]> values; // NG その2 24 unique_lock<decltype(mtx)> lock(mtx); 25 //unique_ptr<uint8_t[]> values; // OK 26 27 while (que.empty()) { 28 cond.wait(lock); 29 } 30 31 values = move(que.front()); 32 que.pop_front(); 33 } 34 35 // valuesに対して処理を行う 36 37 this_thread::yield(); 38 } 39} 40void writer() 41{ 42 for (auto i = 0; i <= 100000; ++i) { 43 try { 44 unique_ptr<uint8_t[]> values(new uint8_t[LENGTH]); 45 46 // valuesに対して処理を行う 47 48 // Critical section. 49 { 50 lock_guard<decltype(mtx)> lock(mtx); 51 52 que.push_back(move(values)); 53 cond.notify_one(); 54 } 55 } 56 catch (exception e) { 57 cout << e.what() << endl; 58 } 59 60 this_thread::yield(); 61 } 62} 63 64int main() 65{ 66 unique_ptr<thread> reader_thread(new thread(reader)); 67 unique_ptr<thread> writer_thread(new thread(writer)); 68 69 writer_thread->join(); 70 reader_thread->join(); // infinity 71 72 return 0; 73}

余談

スレッド間で「1回あたり2MB以上」「秒間25回」程度のデータを送受信する、スマートな方法が思いつかずこのようなコードになっています。
送信前に受信側バッファが一定サイズを超えていない事の確認も必要で、何か良い方法がないものでしょうか……。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

あげられているコードをそのまま実行してみたところ、bad_allocまではいきませんでしたが瞬間的にメモリの使用量が増えます。
単純にスレッドになかなかCPUが割り当てられないタイミングがあって、writerに割り当てられなかったらreaderはキューを全部処理しきって待つだけだけど、readerに割り当てられなかったらwriterはガンガンキューに突っ込んでいくのでメモリが足りなくなるんだと思います。
CPUが割り当てられないタイミングがあるのは別段不思議ではないです。

キューのデータ数が一定数を超えたら処理されるまでwriterを待機させるとか、キューの代わりに固定サイズのリングバッファを作って、writer側も空きバッファが無かったら(readerが処理しなかったら)空きができるまで待機するようにしたらどうでしょうか?

投稿2018/02/08 13:58

編集2018/02/08 14:00
toki_td

総合スコア2850

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

HARQ

2018/02/08 15:49

ご回答ありがとうございます。見直してみると確かにwriterが書き込み続けてますね……。 定義位置の順序を変えただけで挙動が変わる(微妙なタイミングの違いかな?)ので混乱してしまいました。 複数台のPCを用意して確認したところ、それぞれ挙動が異なることから質問自体は勘違いの様です。 と、このままではお恥ずかしい限りなので、同期実行ビジュアライザーでスレッド切り替えの模様を確認してみました。やはり現状のコードではwriter側がreader側より頻繁に動作しており、 cond.wait()で待つことがほぼ無い = readerが間に合っていなかった模様です。 試しに「que.size() < 10 の場合のみ、writerがpush & notifyする」ように変更したところ、正常に動作しているようです。今度はwriter側が追いつかず、reader側がwaitしていましたが……。
toki_td

2018/02/08 16:53

このスレッドのやり取りの前後にどういう入出力があるかわかりませんが、秒間25回(40ms間隔)で安定的に切り替えたいということでしょうか?それくらいの間隔ならタイマーのほうが良いように思いますが。 Windowsでの僕の経験則ですが、2つのスレッドを安定的に相互に動かしたい場合、yieldはOSが必要と判断しなかったら素通りして切り替わらない場合があるので、1msだけsleepして確実にCPUを放棄したほうがうまく切り替わるように思います。
HARQ

2018/02/08 17:41

なるほど。yieldの実装は確かSleep(0)またはSwitchToThreadだったと思うので、Sleep(1)でなければ期待したように切り替わらない可能性は高いですね。 ちなみに実際のwriterはreaderに関係なく毎秒25回動作するスレッド(からのコールバック関数)となります。reader側の処理はかなり重たいので、こちらはこまめにyieldを入れる必要がありそうです。
guest

0

こんにちは。

VC++ 2017でやってみました。結論としてはtoki_tdさんが書かれている通りのようです。

bad_allocも発生しました。途中でque.size()を表示してみたところ、徐々に増えていきました。そこで、writer()側でque.size()が10を超えたら、sleep_for()してみたところ、10個程度までしか増えなくなりました。

送信前に受信側バッファが一定サイズを超えていない事の確認も必要で、何か良い方法がないものでしょうか……。

基本的にはこの内容は妥当な気がします。
ただ、キューのたまり具合に応じて処理負荷を調整する必要があります。
ですので、固定長のFIFOを用いることが多いです。
C++の標準ライブラリのqueue(deque含む)はどこまでもメモリを獲得することがちょっと不思議です。キューサイズに応じた処理をプログラマが自由に作れるようにマルっと任せられているのかも知れません。

condition_variableを使えば簡単ですし。

投稿2018/02/08 14:12

編集2018/02/08 14:13
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yumetodo

2018/02/08 14:32

>C++の標準ライブラリのqueue(deque含む)はどこまでもメモリを獲得することがちょっと不思議です。 std::dequeじゃないコンテナを渡せばいいのでは。もっともコンテナ書くのがだるいのはそうなんだけれども
Chironian

2018/02/08 14:57

これってデキューより多くエンキューされる問題ですので、enqueに手心を加える必要があります。そのような時は、キューのサイズに上限を設けると比較的スマートに回避できます。 以前調べた時はsize()の上限を設定できるSTLコンテナはなかったと思います。 デキューより大幅にエンキューが遅いコンテナでも良いでしょうが、それは処理系や動作環境に強く依存するので宛にはできないと思います。
yumetodo

2018/02/08 15:36

STLの範囲ではdequeとlistしか渡せない気がしますが、boost::static_vectorをいじってリングバッファにしたようなものを・・・(言ってて面倒だなって思う
HARQ

2018/02/08 16:45 編集

ご回答、コメントありがとうございます。普段組み込み系をメインとしている身としてお恥ずかしい限りです(言い訳としてはSTLを使える環境が久しぶりすぎて……)。 実際のコードではコールバック関数としてwriterが実装され、queueに空きがあればreaderへ送信、空きがなければ破棄という動作をします。そのため単純にwriterでqueueのサイズをチェックするだけで良さそうだと考えています(lockしてque.size()が重いので、atomic<int> que_size変数で別途カウント)。 queueに空きがない場合に送信側がblockする実装については、探したところ良さそうな例がありました。 <ミューテックスと複数の条件変数> https://cpprefjp.github.io/article/lib/how_to_use_cv.html
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問