1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C/C++でレガシーコードを無理やり並列実行

Last updated at Posted at 2021-08-08

目的・経緯

C/C++で書かれたレガシーコード(グローバル変数/静的変数を多用した)のロジックがあるのですが、

ロジック部分のソースコードを基本変更せずにマルチスレッド・並列実行してすることで
実行時間を短くすることができないか?

と、いう要望があり、実験してうまくいく方法ができたので、まとめてみました。

状況の補足として

  • レガシーコード部分をリファクタリングしないのは規模が大きすぎて時間・コスト的がかかりすぎるため
  • そのまま並列実行すると、処理途中の内容をグローバル変数に保持するコードとなっているため処理結果が壊れる

という状況。

実行環境

  • Windows10
  • Visual Studio 2019

対応方法

DLLはロード時に、ロード元プロセスのアドレス空間内にマッピングされます。
⇒マッピングされるのは、DLL内に実装されている関数やグローバル変数となります。

DLL毎に異なるアドレスにマッピングされるので、別DLLに同じ関数名やグローバル変数明があっても、別アドレス・別物として扱われます。

上記を利用して、

  • レガシーコードのロジック部分をDLLとしてビルドする
  • DLLをコピー&リネームして複数用意する(並列実行したい数分)
  • 呼出元では各DLLをロードして、複数スレッドでそれぞれのDLL内のロジックを呼び出す

とすることで、レガシーコード内のリファクタリングせずに並列実行を実現。

対応サンプルコード

レガシーコード側

# include <windows.h>
# define EXPORT  extern "C"  __declspec(dllexport)

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved){
    return TRUE;
}

// グローバル変数定義
EXPORT int value = 0;
// DLL公開 関数のプロトタイプ宣言
EXPORT void add(int v);
EXPORT int get_value();

void add(int v) {
    value += v;
}

int get_value() {
    return value;
}
  • 実現できるかの確認用なので、グローバル変数の値を加算していくだけのコード
  • add関数、get_value関数、value変数をDLLとして公開

上記のコードを ダイナミック リンク ライブラリ(DLL) のプロジェクトとしてビル
ドする。
ビルドしてできたDLLをコピーして複数作成する。
この後の例では、target1.dlltarget2.dlltarget3.dllという名前にコピーしてリネーム。

呼び出しコード側

# include <windows.h>
# include <iostream>
# include <thread>

// DLLロード用クラス
class TargetLib
{
public:
	TargetLib(LPCTSTR libpath) {
		// DLLの動的リンク開始
		h_module = LoadLibrary(libpath);
		if (h_module) {
			// DLLの公開関数のアドレス取得
			add = (void(*)(int))GetProcAddress(h_module, "add");
			get_value = (int(*)())GetProcAddress(h_module, "get_value");
			// DLLの公開グローバル変数のアドレス取得
			value = (int*)GetProcAddress(h_module, "value");
		}
	}
	~TargetLib() {
		// DLLの動的リンク終了
		FreeLibrary(h_module);
	}
public:
	HMODULE h_module;
	// DLLの公開関数用の関数ポインタ
	void(*add)(int) = NULL;
	int(*get_value)() = NULL;
	// DLLの公開グローバル変数にポインタ
	int* value = NULL;
};

int data1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int data2[] = { 1, 3, 5, 7, 9 };
int data3[] = { 0, 2, 4, 6, 8, 10 };

int main(int argc, char const* argv[]) {
	int result1 = 0;
	int result2 = 0;
	int result3 = 0;
	// ライブラリをそれぞれロード
	TargetLib target1(L"target1.dll");
	TargetLib target2(L"target2.dll");
	TargetLib target3(L"target3.dll");
	// スレッドでそれぞれ実行
	std::thread thread1([&] {
		std::cout << "thread1 START" << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
		for (int v : data1) { target1.add(v); }
		result1 = target1.get_value();
		std::cout << "thread1 END" << std::endl;
	});
	std::thread thread2([&] {
		std::cout << "thread2 START" << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
		for (int v : data2) { target2.add(v); }
		result2 = target2.get_value();
		std::cout << "thread2 END" << std::endl;
	});
	std::thread thread3([&] {
		std::cout << "thread3 START" << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
		for (int v : data3) { target3.add(v); }
		result3 = target3.get_value();
		std::cout << "thread3 END" << std::endl;
	});
	// スレッドの完了待ち
	thread1.join();
	thread2.join();
	thread3.join();
	std::cout << "join END" << std::endl;
	// 実行結果の出力
	std::cout << "result1 = " << result1 << std::endl;
	std::cout << "result2 = " << result2 << std::endl;
	std::cout << "result3 = " << result3 << std::endl;
	return 0;
}
  • DLLをそれぞれロードして、別々のスレッドで実行しています。

出力結果例

thread1 START
thread2 START
thread3 START
thread3 END
thread2 END
thread1 END
join END
result1 = 55
result2 = 25
result3 = 30

それぞれの resultの値がそれぞれ期待通りの値で返ってきていることがわかる。

※並列実行される個所の文字出力は、タイミングによって混じって表示される場合あり。

補足

実際にどのようにメモリ割り当てされているか確認して見ると以下のようになっていた。

WatchVariable.png

それぞれのDLLが以下のようにマッピングされている

  • target1.dll0x00007ffc9fbd0000アドレス以降にマッピング
  • target2.dll0x00007ffc9fbc0000アドレス以降にマッピング
  • target3.dll0x00007ffc9faf0000アドレス以降にマッピング
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?