Threadの安全な終了について

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
Referia
記事: 24
登録日時: 5年前
住所: 奈良

Threadの安全な終了について

#1

投稿記事 by Referia » 4年前

現在VS2013expressのC++/CLIのフォームでブロック崩しを作成したのですが、
終了時稀に「使用されたパラメータは有効ではありません」というエラーボックスが表示されます。
その時のエラーメッセージは「System.ArgumentException」であり、おそらくThreadexplosionあたりが吐かれたというのも見当が付きました。
やはり終了時にThread::Abort()だけ呼び出すのはダメなのでしょうか。
御意見を頂きたいです。

コード:

#pragma once
#include "ball.h"
#include "bar.h"
#include "Block.h"

namespace blockBreaker {



	/// <summary>
	/// MyForm の概要
	/// </summary>
	public ref class MyForm : public System::Windows::Forms::Form
	{
	private:
		// 各種クラスのインスタンス
		Ball^ ball;
		Bar^ bar;
		Block^ block;

		// キー情報の格納
		Keys key;

		// ウィンドウの画像データ
		Bitmap^ back;
		Graphics^ g;

		// ボールとバーの座標データとブロック座標配列を持つ構造体
		BlockBreakerPointData^ pointDate;

		// データコンテナのロック用
		Mutex^ mutex;

		// 描画スレッド
		Thread^ renderThread;
		// バー更新スレッド
		Thread^ barThread;
		// ボール更新スレッド
		Thread^ ballThread;

		// ブロック削除用変数
		int deleteNumber;

		// フォント
		System::Drawing::Font^ font;

		// ゲーム終了フラグ
		GameEndStatus Gameend = NoneEnd;

		// ボールの射出フラグ
		bool BallShots = false;

		/// <summary>
		/// 必要なデザイナー変数です。
		/// </summary>
		IContainer^  components;

	public:
		MyForm(void)
		{
			InitializeComponent();
			//
			//TODO: ここにコンストラクター コードを追加します
			//

			// ダブルバッファ許可設定
			this->SetStyle(ControlStyles::DoubleBuffer, true);
			this->SetStyle(ControlStyles::UserPaint, true);
			this->SetStyle(ControlStyles::AllPaintingInWmPaint, true);

			// 各種クラスインスタンスの生成と初期化
			ball = gcnew Ball();
			ball->Initialize();
			bar = gcnew Bar();
			bar->Initialize();
			block = gcnew Block();
			block->Initialize();

			// 削除用変数の初期化
			deleteNumber = -1;

			// 背景画像の作成・それに描画するグラフィッククラスの生成
			back = gcnew Bitmap(ClientSize.Width, ClientSize.Height);
			g = Graphics::FromImage(back);

			// キー状態を保存するメンバの初期化
			key = Keys::None;

			// フォントの初期化
			font = gcnew System::Drawing::Font("HG行書体", 40);

			mutex = gcnew Mutex();


			// ゲーム上のデータを集約させた構造体の初期化
			pointDate = gcnew BlockBreakerPointData(mutex);
			pointDate->mutex = mutex;
			{
				pointDate->mutex->WaitOne();
				pointDate->ballPoint = ball->getballPoint();
				pointDate->barRect = bar->getRectangle();
				for (int i = 0; i < BLOCK_NUM; i++){
					pointDate->blockArray[i] = block->getBlockStatus(i);
				}
				pointDate->mutex->ReleaseMutex();
			}


			/***************************************/
			/******  スレッド処理の生成・設定 ******/
			/***************************************/
			barThread = gcnew Thread(gcnew ParameterizedThreadStart(this, &MyForm::BarUpdate));
			ballThread = gcnew Thread(gcnew ParameterizedThreadStart(this, &MyForm::BallUpdate));
			renderThread = gcnew Thread(gcnew ParameterizedThreadStart(this, &MyForm::GameRender));

			// スレッド初回起動
			barThread->Start();
			ballThread->Start();
			renderThread->Start();
		}

	protected:
		/// <summary>
		/// 使用中のリソースをすべてクリーンアップします。
		/// </summary>
		~MyForm()
		{
			if (components)
			{
				delete components;
			}
			barThread->Abort();
			ballThread->Abort();
			renderThread->Abort();

			delete ball;
			delete bar;
			delete block;
		}
	protected:

#pragma region Windows Form Designer generated code
		/// <summary>
		/// デザイナー サポートに必要なメソッドです。このメソッドの内容を
		/// コード エディターで変更しないでください。
		/// </summary>
		void InitializeComponent(void)
		{
			this->SuspendLayout();
			// 
			// MyForm
			// 
			this->AutoScaleDimensions = System::Drawing::SizeF(6, 12);
			this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
			this->ClientSize = System::Drawing::Size(454, 612);
			this->Name = L"MyForm";
			this->Text = L"MyForm";
			this->Load += gcnew System::EventHandler(this, &MyForm::MyForm_Load);
			this->KeyDown += gcnew System::Windows::Forms::KeyEventHandler(this, &MyForm::MyForm_KeyDown);
			this->KeyUp += gcnew System::Windows::Forms::KeyEventHandler(this, &MyForm::MyForm_KeyUp);
			this->ResumeLayout(false);

		}
#pragma endregion
	private: System::Void MyForm_Load(System::Object^  sender, System::EventArgs^  e) {
	}

			 // バーの更新処理
	private: void BarUpdate(Object^ stateInfo) {
		while (Gameend == NoneEnd)
		{
			if (pointDate->mutex->WaitOne())
			{
				bar->UpDate(key);
				pointDate->barRect = bar->getRectangle();

				pointDate->mutex->ReleaseMutex();
			}
			barThread->Sleep(25);
		}
	}

			 // ボールの更新処理
	private: void BallUpdate(Object^ stateInfo) {
		while (Gameend == NoneEnd){
			if (pointDate->mutex->WaitOne())
			{
				deleteNumber = ball->UpDate(*pointDate->barRect, pointDate);
				pointDate->ballPoint = ball->getballPoint();
				// deleteNumberが-50(底に着いた)であった場合
				// すべてのスレッドを止める
				if (deleteNumber < -50){
					EndChange(GameEndStatus::GameOver);
				}
				// もし削除するブロックが存在するならば
				else if (deleteNumber > -1){
					// 削除(という名前のstatus変更:実際にはデータの削除は行わない)
					block->deleteBlocks(deleteNumber);
					deleteNumber = -1;
				}
				// そうでなければ最新の情報に更新
				else{
					for (int i = 0; i < BLOCK_NUM; i++){
						pointDate->blockArray[i] = block->getBlockStatus(i);
					}
				}
				// もしすべてのブロックが破壊されたならすべてのスレッドを止める
				if (block->getSurvivorBlockCount() == 0){
					EndChange(GameEndStatus::GameClear);
				}

				pointDate->mutex->ReleaseMutex();
			}
			ballThread->Sleep(20);
		}
	}

			 // ゲーム描画処理
	private: void GameRender(Object^ state){
		while (1){
			if (pointDate->mutex->WaitOne())
			{

				delete g;		g = nullptr;
				delete back;	back = nullptr;

				back = gcnew Bitmap(ClientSize.Width, ClientSize.Height);
				g = Graphics::FromImage(back);

				switch (Gameend)
				{
				case GameOver:
					// 敗北時の描画
					g->FillRectangle(Brushes::Black, Rectangle(0, 0, ClientSize.Width, ClientSize.Height));
					g->DrawString("GAME OVER", font, Brushes::White, ClientSize.Width / 8 * 2.0f, ClientSize.Height / 2.0f);
					break;
				case GameClear:
					// 勝利時の描画
					g->FillRectangle(Brushes::Black, Rectangle(0, 0, ClientSize.Width, ClientSize.Height));
					g->DrawString("GAME CLEAR", font, Brushes::White, ClientSize.Width / 8 * 2.0f, ClientSize.Height / 2.0f);
					break;
				case NoneEnd:
				default:
					// ゲームボードの描画
					for (int i = 0; i < BLOCK_NUM; i++){
						if (pointDate->blockArray[i]->status == Survivor){
							g->FillRectangle(Brushes::Blue, *pointDate->blockArray[i]->rect);
						}
					}
					g->FillRectangle(Brushes::Black, *pointDate->barRect);
					g->FillEllipse(Brushes::Black, pointDate->ballPoint->X, pointDate->ballPoint->Y, BALL_WIDTH, BALL_HEIGHT);
					break;
				}
				this->BackgroundImage = back;
				pointDate->mutex->ReleaseMutex();
			}
			renderThread->Sleep(20);
		}
	}

	private: System::Void MyForm_KeyDown(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
		if (e->KeyCode == Keys::Left || e->KeyCode == Keys::Right || e->KeyCode == Keys::Space){
			if (pointDate->mutex->WaitOne())
			{
				key = e->KeyCode;
				if (key == Keys::Space && !BallShots){
					ball->GameStart(key);
					BallShots = true;
				}
				pointDate->mutex->ReleaseMutex();
			}
		}
	}

	private: void EndChange(GameEndStatus s){
		Gameend = s;
	}

	private: System::Void MyForm_KeyUp(System::Object^  sender, System::Windows::Forms::KeyEventArgs^  e) {
		if (e->KeyCode == Keys::Left || e->KeyCode == Keys::Right){
			if (pointDate->mutex->WaitOne())
			{
				key = Keys::None;
				pointDate->mutex->ReleaseMutex();
			}
		}
	}
	};
}

YuO
記事: 941
登録日時: 9年前
住所: 東京都世田谷区

Re: Threadの安全な終了について

#2

投稿記事 by YuO » 4年前

全てのスレッド (.NET Framework (CLI) に限らない) について,他のスレッドから強制終了させることは適当ではありません。
大原則として,「スレッドに対して終了をお願いし,それをスレッドが受け付けて自分で終了処理をする」必要があります。
CLIの場合,System::Threading::ManualResetEventや,volatileなbool型変数などが使えると思います。

ただし,CLI環境において,そもそもThreadクラスを直接使う必要性が感じられないのも事実です。
現在の主流はSystem::Threading::Tasks::Taskですし,.NET Framework 4.0より前でもThreadPoolクラスがあります。
# DelegateのBeginInvokeはC++/CLIで用意されたかはわからないので除いておきます。
通常はスレッドプールで十分であり,Threadクラスを直接使うのはCOM interopの関係でSTA/MTAを指定したい場合など,とても限られた状況になります。

sleep

Re: Threadの安全な終了について

#3

投稿記事 by sleep » 4年前

まず、この機会に スレッドについて基礎から再入門された方が良さそうですね。
スレッドの基礎

それと、指摘したいことがあります。
理解できていない技術についての調査は、他の技術的要素を含まない小さなテストプログラムから始めるようにしてみてください。
理由は、動かなくなっている原因が想像していたものと全く別である可能性があるからです。
全然違う原因を延々と調べ続けてしまう様な 迷走した状況に陥ってしまうことを避けるためです。

スレッドの終了のさせ方を示すテストコードを一例として載せておきます。
(全てのスレッドの終了条件を「while (Gameend == NoneEnd) を抜けた場合、ということにしています。」)
同期処理については、今回触れてません。
基礎的なところが理解できて 初めて意味が分かるようになるので、そちらについては日進月歩でがんばってみてください。

コード:

using namespace System;
using namespace System::Threading;
using namespace System::Windows::Forms;

ref class MyWindow : public Form
{
private:
	bool Gameend;
	bool NoneEnd;
	Thread ^barThread;
	Thread ^ballThread;
	Thread ^renderThread;

protected:
	virtual void MyWindow::OnClosed(System::EventArgs ^e) override
	{
		Gameend = true;  //終了条件として、NoneEndと異なる値にする

		//各スレッドの終了を待機
		barThread->Join();     Console::WriteLine("終了: BarUpdate");
		ballThread->Join();    Console::WriteLine("終了: BallUpdate");
		renderThread->Join();  Console::WriteLine("終了: GameRender");
	}

public:
	MyWindow::MyWindow()
	{
		Gameend = false;
		NoneEnd = false;

		barThread = gcnew Thread(gcnew ThreadStart(this, &MyWindow::BarUpdate));
		ballThread = gcnew Thread(gcnew ThreadStart(this, &MyWindow::BallUpdate));
		renderThread = gcnew Thread(gcnew ThreadStart(this, &MyWindow::GameRender));

		//バックグラウンドスレッドにする
		//barThread->IsBackground = true;
		//ballThread->IsBackground = true;
		//renderThread->IsBackground = true;

		barThread->Start();    Console::WriteLine("開始: BarUpdate");
		ballThread->Start();   Console::WriteLine("開始: BallUpdate");
		renderThread->Start(); Console::WriteLine("開始: GameRender");
	}
	MyWindow::~MyWindow()
	{
		Console::WriteLine("リソース解放");
	}
	void BarUpdate()
	{
		while (Gameend == NoneEnd)
		{
			Thread::Sleep(2000);
			Console::WriteLine("BarUpdate");
		}
	}
	void BallUpdate()
	{
		while (Gameend == NoneEnd)
		{
			Thread::Sleep(1000);
			Console::WriteLine("BallUpdate");
		}
	}
	void GameRender()
	{
		while (Gameend == NoneEnd)
		{
			Thread::Sleep(500);
			Console::WriteLine("GameRender");
		}
	}
};

[STAThreadAttribute]
int main()
{
	MyWindow ^window = gcnew MyWindow();
	Application::Run(window);
	return 0;
}

sleep

Re: Threadの安全な終了について

#4

投稿記事 by sleep » 4年前

今回は ユーザー側のClosedイベントを追加していないので呼び出すものが無く 不要ですが、
もし、Closedイベントを追加しており、呼び出す必要がある場合は
Form::OnClosedの実装からキックされているので、以下のように overrideしたOnClosed 関数の中に その呼び出しを含めてください。

Form::OnClosed メソッド

コード:

virtual void MyWindow::OnClosed(System::EventArgs ^e) override
{
	・・・

	Form::OnClosed(e);
}

Referia
記事: 24
登録日時: 5年前
住所: 奈良

Re: Threadの安全な終了について

#5

投稿記事 by Referia » 4年前

YuOさん、sleepさん御指摘、御回答ありがとうございます
YuO さんが書きました: ただし,CLI環境において,そもそもThreadクラスを直接使う必要性が感じられないのも事実です。
現在の主流はSystem::Threading::Tasks::Taskですし,.NET Framework 4.0より前でもThreadPoolクラスがあります。
Tasksクラスというものがあったのですね・・・
調べてみようと思います!
sleep さんが書きました: まず、この機会に スレッドについて基礎から再入門された方が良さそうですね。
スレッドの基礎
スレッド処理に関してはほとんど手を付けておらず、興味本位で手を出していました
ですので教えてもらったサイト等を参考に勉強します。
sleep さんが書きました: それと、指摘したいことがあります。
理解できていない技術についての調査は、他の技術的要素を含まない小さなテストプログラムから始めるようにしてみてください。
理由は、動かなくなっている原因が想像していたものと全く別である可能性があるからです。
全然違う原因を延々と調べ続けてしまう様な 迷走した状況に陥ってしまうことを避けるためです。
御指摘ありがとうございます、当初はスレッド処理を含まないものをして制作を行っており
完成後にスレッド処理を興味本位で追加したので、今後は小規模なテストプログラムを作成し
その後に導入という風にしようと思います
今回のプログラムに関してもテストプログラムを作成し、もう一度作成し直そうと思います

Referia
記事: 24
登録日時: 5年前
住所: 奈良

Re: Threadの安全な終了について

#6

投稿記事 by Referia » 4年前

申し訳ありませんが、一旦解決とさせていただきます。
理由としましては自分の学習がいつ終わるか分からないためです。

閉鎖

“C言語何でも質問掲示板” へ戻る