C++ Linux

C++のマルチスレッド環境における問題とvalgrindを用いた検出方法

投稿日:

私は仕事でC++を使用してソフトウェアを実装しています。

今回は、C++マルチスレッドプログラミングで発生する問題点とその検出方法を(自分のメモも兼ねて)記述します。

環境は以下になっております。

  • OS:Ubuntu 14.04
  • 言語:C++11

 

データ競合と競合状態

マルチスレッド環境においては、以下の2つの問題が常につきまわります。

  1. データ競合
  2. 競合状態

これらの概念を簡単に説明いたします。次のソースをご覧ください。

#include <thread>
#include <stdio.h>
#include <unistd.h>


void work (int *p_data) {
  for (int i = 0; i < 100; i++) {
    *p_data = *p_data + 2;
    sleep(0);  // スレッドの切り替えをを強制的に起こす
  }
  printf("%d\n", *p_data);
}

int main() {
  int data = 0;
  std::thread th1(work, &data);
  std::thread th2(work, &data);
  th1.join();
  th2.join();
  return 0;
}

上記ソースにおいてprintfで出力される数値の仕様を「200400」だと規定します。

上記のソースをコンパイルして複数回実行した結果は以下です。

user@hostname:~$ ./a.out 
400
400
user@hostname:~$ ./a.out 
398
400
user@hostname:~$ ./a.out 
398
400
user@hostname:~$ ./a.out 
400
400
user@hostname:~$ ./a.out

仕様通りの結果が出力されず、しかも実行毎に動作が違うことが確認できます。

このソースは、データ競合と競合状態、どちらも発生しています。

データ競合とは、簡単に言うと、
同一のメモリ(変数)を複数のスレッド(及びプロセス)が書き込み可能である状態を指します。
データ競合時には、そのデータに関わる動作は言語によって規定されますが、C++においては未定義と規定されています。
ソースでは、2つのスレッドth1,th2で参照されるdataの値は、C++の使用上未定義になります。データ競合が発生している変数は、どのような値を取りうるかは(C++においては)定義できません。今回の例では「たまたま」値が正しく表示されていますが、厳密に言えば、この値は信用できない値になります。

一方、競合状態とは、簡単に言うと、
同じプログラムでも、マルチスレッドの処理順序に依存して、動作が変化する状態を指します。
ソースでは、8行目のprintfが、仕様として定めた標準出力「200400」を必ずしも出力するとは限らない、ということになります。スレッドの処理順番によって、どの時間のdataの値を参照するかが変わってしまうからです。

データ競合の検出方法

データ競合については、簡単に検出する方法があります。
C++の動的解析ツール「valgrind」を利用します。
メモリリーク検出ツールでおなじみかと思いますが、実はvalgrindには、Data raceを検出するhelgrindというツールが付属しています。

インストールは下記コマンドで行います。

user@hostname:~$ apt-get install valgrind

実行時には、以下のようにコマンドを入力します。

user@hostname:~$ valgrind --tool=helgrind ./a.out 
==48== Helgrind, a thread error detector
==48== Copyright (C) 2007-2013, and GNU GPL'd, by OpenWorks LLP et al.
==48== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==48== Command: ./a.out
==48== 

====== (省略)======

==48== Possible data race during write of size 4 at 0xFFF00063C by thread #3
==48== Locks held: none
==48==    at 0x400D8F: work(int*) (in /tmp/a.out)
==48==    by 0x4022ED: void std::_Bind_simple<void (*(int*))(int*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (in /tmp/a.out)
==48==    by 0x4021F8: std::_Bind_simple<void (*(int*))(int*)>::operator()() (in /tmp/a.out)
==48==    by 0x402191: std::thread::_Impl<std::_Bind_simple<void (*(int*))(int*)> >::_M_run() (in /tmp/a.out)
==48==    by 0x4EEEA5F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==48==    by 0x4C30FA6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==48==    by 0x535F183: start_thread (pthread_create.c:312)
==48==    by 0x5672FFC: clone (clone.S:111)
==48== 
==48== This conflicts with a previous write of size 4 by thread #2
==48== Locks held: none
==48==    at 0x400D8F: work(int*) (in /tmp/a.out)
==48==    by 0x4022ED: void std::_Bind_simple<void (*(int*))(int*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (in /tmp/a.out)
==48==    by 0x4021F8: std::_Bind_simple<void (*(int*))(int*)>::operator()() (in /tmp/a.out)
==48==    by 0x402191: std::thread::_Impl<std::_Bind_simple<void (*(int*))(int*)> >::_M_run() (in /tmp/a.out)
==48==    by 0x4EEEA5F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==48==    by 0x4C30FA6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==48==    by 0x535F183: start_thread (pthread_create.c:312)
==48==    by 0x5672FFC: clone (clone.S:111)
==48==  Address 0xfff00063c is on thread #1's stack
==48==  in frame #4, created by main (???)
==48== 
==48== 
==48== For counts of detected and suppressed errors, rerun with: -v
==48== Use --history-level=approx or =none to gain increased speed, at
==48== the cost of reduced accuracy of conflicting-access information
==48== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 33 from 28)

Possible data race〜 によって、Data raceの可能性のあるメモリを通知してくれます。
また、Data raceの可能性のあるメモリを参照する処理において排他がされているか(Locks held:)も合わせて通知してくれます。noneの場合は、排他がされていないため、適切な手段(mutexなど)を使用して排他を行う必要があります。

終わりに

マルチスレッドプログラミングのデバッグ時には相当な時間と体力を要します。それは、データ競合と競合状態について、問題の切り分けが難しいためです。
今回紹介したvalgrindを使用してデータ競合を検出することで、頭を悩ませるのは競合状態の解決だけに絞っていきたいですね。

参考リンク

https://qiita.com/yohhoy/items/00c6911aa045ef5729c6

-C++, Linux
-,

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

関連記事

Docker上でnginx-proxy他を使ってSSL対応マルチドメインサーバ環境の構築

今回は、nginx-proxyを使用し、SSLに対応したマルチドメイン環境を構築します。 追記(2017/10/08):作成したdocker-compose.ymlをGithubに公開しました。目次の …

pthread_cancelすると何が行われるのか

仕事でのメモ。 目次1 pthread_cancel2 pthread_cancelで何が行われるのか2.1 シグナル送出2.2 注意2.3 ソースコードを読む2.4 pthread_cancel時の …

Dockerを使用した簡単なC++実行環境の構築

今回は、C++の機能を調査するための簡単なテスト環境を、Dockerを用いて構築する手順を解説します。 目次1 要件2 仕様3 実装3.1 元になるイメージ3.2 Dockerfileの作成3.3 実 …

Linux+RustでOS自作〜環境構築編〜

目次1 概要2 環境構築2.1 ツール一覧2.2 ツールのインストール3 終わりに4 参考サイト様4.0.1 関連 概要 先日、「30日でできる! OS自作入門」を購入しました。 30日でできる! O …

Docker上でWordPressとnginx-proxyを連携(SSL対応)

今回は、Docker上でWordpressサーバを構築し、SSLに対応したnginx-proxyとの連携を行います。 追記(2017/10/08):作成したdocker-compose.ymlをGit …