目的・経緯
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.dll
、target2.dll
、target3.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の値がそれぞれ期待通りの値で返ってきていることがわかる。
※並列実行される個所の文字出力は、タイミングによって混じって表示される場合あり。
補足
実際にどのようにメモリ割り当てされているか確認して見ると以下のようになっていた。
それぞれのDLLが以下のようにマッピングされている
-
target1.dll
が0x00007ffc9fbd0000
アドレス以降にマッピング -
target2.dll
が0x00007ffc9fbc0000
アドレス以降にマッピング -
target3.dll
が0x00007ffc9faf0000
アドレス以降にマッピング