barus's diary

とても真面目なblogですにゃ

マルチスレッド処理(thread)VC++でDLL作成しC#で利用 for VS2015 Express for Desktop

マルチ処理(thread)VC++でDLL作成し利用 VC++ for VS2015 Express for Desktop - barus's diary

 

の続き。

 

VC++でDLLを作成し、C#側でDLLを利用した際のメモ。

 

button1を押すと、5つのマルチスレッドが走り

それぞれの途中処理経過をプログレスバーに表示する。

 

f:id:hatakeka:20170608183825p:plain

 

 

 

[ GUI ] →  [DLL]

 C#              VC++

 

表示部分はC#の方が、個人的に使いやすいのでC#で作りました。

 

 

今回、段階を踏んで作成した。

 

 

ステップ1 C#コンソールアプリケーションからVC++作成のDLLを呼び出す。

 

ステップ2 C#のGUIからVC++作成のDLLを呼び出す。

 

ステップ3 C#のGUIからVC++作成のDLLをマルチスレッドで呼び出す。

 

いきなり、ステップ3は難しかったのでステップ1~3を踏まえました。

後日、忘れたころに見直す時に役に立つかと思います。

 

プロジェクトは3つ作成する。

(1)Csharp_Console  ・・・コンソールアプリケーション

(2)Csharp_Thread   ・・・GUI

(3)VCplus_DLL    ・・・DLL(前回とほぼ同じ)

 

ファイルは以下のような感じとなっています。

ほとんどのファイルは自動的に作成されるので

修正するファイル以外は触らないでOK。

 

 

 

 

Csharp_Console  ・・・コンソールアプリケーション

          Program.csを編集

         f:id:hatakeka:20170608190431p:plain

Csharp_Thread   ・・・GUI

          Form1.csを編集 

         f:id:hatakeka:20170608190319p:plain

VCplus_DLL    ・・・DLL

         Thread_samp1.cpp を編集

         Thread_samp1.h  を編集

        f:id:hatakeka:20170608190812p:plain

 

 

呼び出すDLLは全て共通です。

 

Thread_samp1.cpp 
// Thread_samp1.cpp : DLL アプリケーション用にエクスポートされる関数を定義します。
//

#include "stdafx.h"


#include <string.h>
#include <iostream>
#include <windows.h>
#include <time.h>
#include <stdlib.h>
#include <random>

using namespace std;

#ifdef __cplusplus
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllexport)
#endif


class worksamp
{
public:
	int _id;
	worksamp();
	~worksamp();
	//プロジェクト->プロパティ->構成プロパティ->全般 の中にある
	//文字セットを[Unicode 文字セットを使用する]から[マルチバイト文字セットを使用する]
	void hevy_work(int thread_number, int *pintbar, char *pchar);

};

worksamp::worksamp() {}
worksamp::~worksamp() {}



void worksamp::hevy_work(int thread_number, int *pintbar, char *pchar)
{
	int i = 0, j = 0;

	//------------------------
	//一般的な乱数 
	//------------------------
	//srand(time(NULL));

	//------------------------
	//乱数発生
	//------------------------
	std::random_device rd;
	//メルセンヌ・ツイスターの使用
	std::mt19937 mt(rd());



	int cnt_rnd = mt() % 500 + 1;

	char buf[100];
	
	
	double bar=0.0;

	_id   = thread_number;
	*pintbar = 0;

	for (i = 0; cnt_rnd > i; i++)
	{
		
		Sleep(5);
		bar = (double)i/cnt_rnd*100;
		*pintbar = bar;

		 //-----------------------------------
		//sprintf使用する際
		//プロジェクト>プロパティ>C/C++>全般>SDLチェックをイイエにする。
		//-----------------------------------
		sprintf_s(buf, 100, "pid= %d, i=%3d/%3d(%03.0lf%%)", thread_number, i, cnt_rnd, bar);
		printf("DLL側の処理 %s\n", buf);
		strcpy_s(pchar, strlen(buf) + 1, buf);

	}

	*pintbar = bar;
	sprintf_s(buf, 100, "pid= %d, i=%3d/%3d(%03.0lf%%)", thread_number, i, cnt_rnd, bar);
	strcpy_s(pchar, strlen(buf) + 1, buf);

	printf("DLL側の処理 pid= %d 処理の終了\n", thread_number);

}




worksamp work;


DLLEXPORT void  mydll_main(int thread_number, int *pintbar, char *pchar)
{
	work.hevy_work(thread_number, pintbar, pchar);
}




Thread_samp1.h

#pragma once

#include <windows.h>
#include <string.h>

#ifdef __cplusplus
#define DLLEXPORT extern "C" __declspec(dllexport)
#else
#define DLLEXPORT __declspec(dllexport)
#endif

//------------------------------------------------------------------------------------------------
//VC2015より作成。マルチバイト文字を使用。
//コンパイル時は、プロジェクト>全般>文字セットをUnicode文字からマルチバイト文字に変更した。
//処理をDLL化、thread_numberは作成したID、途中経過の情報をpint,pintbar,pcharより取得
//------------------------------------------------------------------------------------------------
DLLEXPORT void mydll_main(int thread_number, int *pintbar, char *pchar);





 

作成したDLLは、コンソールアプリケーション、GUI

実行形式と同じフォルダ に配置して下さい。

C#の場合は、VC++のように、*libや、~hを移動させる必要はありません。

 

 

ステップ1 C#コンソールアプリケーションからVC++作成のDLLを呼び出す。

DLL呼び出し部分でVC++と違うのが

intとcharのポインタの型がIntPtrとStringBuilderにかわっていることです。 

Program.cs

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices;// DllImportに必要 namespace Csharp_Console { class Program { [DllImport("Thread_samp1.dll", CallingConvention = CallingConvention.Cdecl)] private extern static void mydll_main(int thread_number, IntPtr pintbar, StringBuilder pchar); static void Main(string[] args) { Dirty_Work dirtywork; dirtywork = new Dirty_Work(); dirtywork.dowork(); } } class Dirty_Work { /* //----------------------- 参考:http://rokujo.hatenadiary.com/entry/2015/07/21/135938 参考:http://qiita.com/ask/items/ee2ff5b8706effc0c3d8 参考:Win32 APIやDLL関数に構造体を渡すには? http://www.atmarkit.co.jp/fdotnet/dotnettips/026w32struct/w32struct.html 参考:Win32 APIやDLL関数を呼び出すには? http://www.atmarkit.co.jp/fdotnet/dotnettips/024w32api/w32api.html char* は文字列なので C# 側からは string を渡してやる DLLEXPORT void mydll_main(int thread_number, int *pint, int *pintbar, char *pchar); C#には IntPtr という型があります。これは汎用的なポインタを表す型で、ほぼ void* と同義です。 具体的には、IntPtrの変数に Marshal.AllocHGlobalで必要なサイズのメモリを確保し、それをC++のDLLに渡します。 さらにMarshal.ReadInt16(必要な型によって異なる)などで変換後、 確保したメモリをMarshal.FreeHGlobalで解放する、と3段階の面倒なプロセスを経なければいけません。 short ConvertToShort(string str) { IntPtr buffer = new IntPtr(); buffer = Marshal.AllocHGlobal(2); _ConvertToShort(str, buffer); short sval = Marshal.ReadInt16(buffer); Marshal.FreeHGlobal(buffer); return sval; } //----------------------- */ [DllImport("Thread_samp1.dll", CallingConvention = CallingConvention.Cdecl)] private extern static void mydll_main(int thread_number, IntPtr pintbar, StringBuilder pchar); public Dirty_Work() { } //mydll_main public void dowork() { const int NUM = 1; int[] arr = new int[NUM]; // アンマネージ配列のメモリを確保 IntPtr pintbar = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * NUM); //文字列 StringBuilder pchar = new StringBuilder(); // 引数でポインタを渡す mydll_main(0, pintbar, pchar); // マネージ配列へコピー Marshal.Copy(pintbar, arr, 0, NUM); // アンマネージ配列のメモリを解放 Marshal.FreeCoTaskMem(pintbar); Console.Write("from dll int "); foreach (int n in arr) { Console.Write(n + " "); } Console.WriteLine("\n"); Console.WriteLine("from dll str = {0}", pchar); } }//class }

 実行すると以下のような感じ

f:id:hatakeka:20170608192845p:plain

 

ステップ2 C#のGUIからVC++作成のDLLを呼び出す。

 
f:id:hatakeka:20170608183825p:plain

ステップ1ではC#からDLLの呼び出し動作テストをしたが
ステップ2では、コンソールアプリケーションではない
スレッドでもない場合のDLLの呼び出しテスト

buttron2を押した際の処理は太字部分。
Dirty_Workクラスはbutton1と共に共通。


Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;


using System.Runtime.InteropServices;// DllImportに必要
using System.Threading; //スレッド



namespace CsharpThread
{
    public partial class Form1 : Form
    {

        //時間
        [DllImport("winmm.dll", EntryPoint = "timeGetTime")]
        public static extern long timeGetTime();

        //スレッドで処理するクラス
        Dirty_Work Dirty_Work;

        //スレッド
        private Thread[] thread_work;
        
        //スレッド数
        public const int MAX_THREAD = 5;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //DLLの関数に投げるintのポインタ
            IntPtr[] pintbar = new IntPtr[MAX_THREAD];

            //DLLの関数に投げるcharのポインタ
            StringBuilder[] pchar = new StringBuilder[MAX_THREAD];


            //autobaibai.set(textBox14.Text, textBox13.Text, txt);
            thread_work = new Thread[MAX_THREAD];


            progressbar_reset();//プログレスバーリセット

            //-------------------------------------------------------
            //thread.Suspend(); // スレッドの一時停止
            //thread.Resume(); // スレッドの再開
            //thread.Join(); // スレッドの処理が終了するまで待つ
            //thread.Abort(); // スレッドの強制終了
            //-------------------------------------------------------

            //-----------------------------
            //フォアグラウンド・スレッドとはそのスレッドが動作している限り、
            //アプリケーションが終了しないスレッドのことである。
            //そして、この逆が「バックグラウンド・スレッド」になる。
            //バックグラウンド・スレッドが動作中にメインのスレッドが終了すれば、
            //バックグラウンド・スレッドは強制的に終了し、アプリケーションも終了する。
            //-----------------------------
            for (int i = 0; MAX_THREAD > i; i++)
            {

                const int NUM = 1;
                int[] arr = new int[NUM];
                
                // アンマネージ配列のメモリを確保
                pintbar[i] = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * NUM);

                //文字列
                pchar[i] = new StringBuilder();
                

                Dirty_Work = new Dirty_Work(i, pintbar[i], pchar[i]);
                thread_work[i] = new Thread(Dirty_Work.dowork);
                thread_work[i].IsBackground = true;
                thread_work[i].Start();
            }

            int cnt = 0;

            //スレッドの途中経過をモニター
            for (int i = 0; MAX_THREAD > i; i++)
            {
                Stoptime(100);

                if (thread_work[i].IsAlive)
                {
                    const int NUM = 1;
                    int[] arr = new int[NUM];

                    // マネージ配列へコピー
                    Marshal.Copy(pintbar[i], arr, 0, NUM);

                    Console.WriteLine("c########## pid={0}, {1} {2}\n", i, arr[0], pchar[i]);

                    progressbar_show(i, arr[0]);
                }
                else
                {
                    //スレッド終了
                    cnt++;
                    if (cnt == MAX_THREAD) break;
                }
            }


            Console.WriteLine("button1_Click()\n");
        }

        public void progressbar_reset()
        {
            //コントロールを初期化する
            progressBar1.Minimum = 0;
            progressBar1.Maximum = 100;
            progressBar1.Value = 0;
            label1.Text = "0";
            progressBar2.Minimum = 0;
            progressBar2.Maximum = 100;
            progressBar2.Value = 0;
            label2.Text = "0";
            progressBar3.Minimum = 0;
            progressBar3.Maximum = 100;
            progressBar3.Value = 0;
            label3.Text = "0";
            progressBar4.Minimum = 0;
            progressBar4.Maximum = 100;
            progressBar4.Value = 0;
            label4.Text = "0";
            progressBar5.Minimum = 0;
            progressBar5.Maximum = 100;
            progressBar5.Value = 0;
            label5.Text = "0";

        }

        public void progressbar_show(int i, int bar)
        {
            switch (i)
            {
                case 0:
                    progressBar1.Value = bar;
                    label1.Text = bar.ToString() + "%";
                    break;
                case 1:
                    progressBar2.Value = bar;
                    label2.Text = bar.ToString() + "%";
                    break;
                case 2:
                    progressBar3.Value = bar;
                    label3.Text = bar.ToString() + "%";
                    break;
                case 3:
                    progressBar4.Value = bar;
                    label4.Text = bar.ToString() + "%";
                    break;
                case 4:
                    progressBar5.Value = bar;
                    label5.Text = bar.ToString() + "%";
                    break;
            }
        }

        public void Stoptime(long st)
        {
            long lngst;
            lngst =timeGetTime();
            while (timeGetTime() - lngst < st)
            {
                Application.DoEvents();
            }
        }

        //スレッドなしの場合のテスト
        private void button2_Click(object sender, EventArgs e)
        {

            const int NUM = 1;
            int[] arr = new int[NUM];

            // アンマネージ配列のメモリを確保
            IntPtr pintbar = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * NUM);

            //文字列
            StringBuilder pchar = new StringBuilder();

            Dirty_Work = new Dirty_Work(0, pintbar, pchar);
            Dirty_Work.dowork();

        }
    }

    class Dirty_Work
    {
        
        [DllImport("Thread_samp1.dll", CallingConvention = CallingConvention.Cdecl)]
        private extern static void mydll_main(int thread_number, IntPtr pintbar, StringBuilder pchar);

        private int _thrednumber;

        // アンマネージ配列のメモリを確保
        IntPtr _pintbar;

        //文字列
        StringBuilder _pchar;

        public Dirty_Work(int thrednumber, IntPtr pintbar, StringBuilder pchar)
        {
            _thrednumber = thrednumber;
            _pintbar = pintbar;
            _pchar = pchar;
        }
        

        public void dowork()
        {
            const int NUM = 1;
            int[] arr = new int[NUM];
            /*
            const int NUM = 1;
            int[] arr = new int[NUM];


            // アンマネージ配列のメモリを確保
            _pintbar = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * NUM);

            //文字列
            _pchar = new StringBuilder();
            */

            //DLLの関数
            mydll_main(_thrednumber, _pintbar, _pchar);

            // マネージ配列へコピー
            Marshal.Copy(_pintbar, arr, 0, NUM);


            // アンマネージ配列のメモリを解放
            Marshal.FreeCoTaskMem(_pintbar);


            Console.Write("from dll int ");

            foreach (int n in arr)
            {
                Console.Write(n + " ");
            }

            Console.WriteLine("\n");


            Console.WriteLine("from dll str = {0}", _pchar);

        }
        
    }//class

}

 

button2を押すと、出力は以下のような感じになる。

f:id:hatakeka:20170608194139p:plain

 

ステップ3 C#のGUIからVC++作成のDLLをマルチスレッドで呼び出す。

 

button1を押した際の処理。

ソースコードはステップ2と同じ。

 

 

スレッドの途中経過をプログレスバーに表示している。

 

f:id:hatakeka:20170608183825p:plain

 

注目点は 

thread_work[i].IsBackground = true;
thread_work[i].IsAlive

IsBackgroundプロパティをTrueにすると、アプリケーションが閉じたら

スレッドのプロセスを強制終了させる。

IsAliveプロパティは、スレッドの生存確認。

 

 

C#からDLL呼び出す際、

ポインタを使うのが面倒くさい処理が必要となっているのが

ポイントだろうと思う。

 

やはり、全てVC++で作ったほうがスッキリするのだろうか

でもGUIの作りやすさは捨てがたい。・・・・

 

あと、構造体も定義出来るみたいだが、調べるのも面倒くさいw。

 

 

実際に動かすと、

終了時に100%で終わらせるようにしたかったが

中途半端な数字で終わるので気持ちが悪い・・・。

 

 

 

まあ、とりあえず以上で

VC++でDLL作成し、C#側でマルチスレッドを動かすことが出来ることを

確認出来た。

 

 

 

 

 

以上。