質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

1回答

23196閲覧

タスクをキャンセルさせてからフォームを閉じる

matsu1

総合スコア19

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

0クリップ

投稿2016/08/05 01:23

編集2016/08/05 04:08

###前提・実現したいこと
お世話になります。
C#でフォームを閉じるボタンを押した場合に、その時点で非同期で動作しているタスクAを終了させてから
フォームを閉じたいと思っています。

###発生している問題・エラーメッセージ
上記を以下の処理フローで実現しようとしました。
1.フォームを閉じるボタンを押す。
2.App_FormClosing(object sender, FormClosingEventArgs e)のイベントハンドラが呼ばれる。
3.その中でe.Cancel = true;をしてフォームを閉じるのをキャンセルする。
4.非同期タスクAをキャンセルしにいく。
5.非同期タスクAがキャンセルされるまで、キャンセル待ちタスクBを別途立ち上げて待つ。
6.非同期タスクAがキャンセルされると、キャンセル待ちタスクBが処理完了する。
7.e.Cancel = false;をしてフォームを閉じさせる。

しかし、結果はe.Cancel = false;が実行されているにも関わらずフォームは閉じませんでした。

###該当のソースコード
----------------------------------- 処理の流れ -----------------------------------

1.非同期タスクAが動作している状態で、フォームの閉じるボタンを押すとApp_FormClosing()が呼ばれる。
2.タスクAが完了状態になってからフォームを閉じたいので、App_FormClosing()内でe.Cancel = true;
により一旦閉じる処理をキャンセルする。

3.非同期タスクA:TaskA()は2s周期で何らかの処理を行っており、同時に
m_CTS.Token.IsCancellationRequestedによりタスクキャンセル命令が来ていないかチェックしている。

4.UI側からCancelTaskA()により、タスクAをキャンセルする。
直後にUI側ではキャンセル待ちタスクBを起動し、タスクAがキャンセルにより実行完了状態になるのを
GetIsTaskACompleted()でチェックしながら待つ。

5.UI側からのキャンセル命令によりタスクAにて、m_CTS.Token.IsCancellationRequestedがtrueになり、
タスクAは実行完了する。

6.タスクAの実行完了により、キャンセル待ちタスクBも待ち終了する。
7.e.Cancel = false;によりフォームを閉じる

----------------------------------- UI側 -----------------------------------

/**
@brief 閉じるボタンを押すと呼ばれるイベントハンドラ
*/
async void App_FormClosing(object sender, FormClosingEventArgs e) {

/* 閉じる処理をキャンセルする
非同期タスクAをキャンセルする
*/
e.Cancel = true;
CancelTaskA();

/* キャンセル待ちタスクBを起動する */
await Task.Factory.StartNew(() => {
while (true) {
Thread.Sleep(500);

/* 非同期タスクAがキャンセルにより実行完了すればキャンセル待ち終了 */
if (GetIsTaskACompleted()) {
break;
}
}
});

/* フォームを閉じる */
e.Cancel = false;
}

----------------------------------- 非同期タスクA側 -----------------------------------

/**
@brief 非同期タスクA用キャンセルトークンソース
*/
private CancellationTokenSource m_CTS new CancellationTokenSource();

/**
@brief 非同期タスクA
*/
private async Task TaskA()
{
await Task.Factory.StartNew(() => {
while (true) {
if (m_CTS.Token.IsCancellationRequested) {
break;
}
Thread.Sleep(2000);
何らかの処理;
}
}, m_CTS.Token);
}

/**
@brief 非同期タスクAをキャンセルする
*/
public void CancelAllTask()
{
m_CTS.Cancel();
}

/**
@brief 非同期タスクAが実行完了したかどうかを得る
*/
public bool GetIsTaskACompleted()
{
return m_TaskA.IsCompleted;
}

###試したこと
非同期タスクAはCancelTaskA()によりキャンセルされ、実行完了し、GetIsTaskACompleted()により
正しく実行完了が検知されることは確認しました。
その後、e.Cancel = false;も実行されるのですがフォームが閉じません。
App_FormClosing()はUIスレッドが呼ぶ関数ですが、キャンセル待ちタスクBの実行でタスクコンテキストが
そちらに移行し、タスクB完了時にもコンテキストがUIに戻っていない?ためにe.Cancel = false;でも
フォームが閉じないのか・・・。それに関して色々と調べてみましたが、解にたどり着けず、
わかる方いらっしゃいましたらご教授をお願いいたします。

代替案として、e.Cancel = false;の代わりにthis.Close()でフォームを閉じる処理を行わせると
再度App_FormClosing()が呼ばれますが、一度this.Close()を実行したら適当なフラグを立てておき
再度App_FormClosing()呼ばれた際にそのフラグが立っているとe.Cancel = false;のみ実行する、
という処理をすればフォームは閉じます。

###補足情報(言語/FW/ツール等のバージョンなど)
C#,VisualStudio2015

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

根本的にTaskの使い方がおかしいです。

まず、

  • Task.Factory.StartNewをawaitしているが、App_FormClosingはasyncメソッドなのか?
  • 「非同期タスクAがキャンセルされるまで、キャンセル待ちタスクBを別途立ち上げて待つ。」とあるが、非同期タスクAのキャンセル処理(== CancelTaskAのこと?)は非同期処理なのか?
  • 非同期処理を待機しているのにwhileループで同期的に待機しているのは何故か?

これらが曖昧なので、明らかにして下さい。

おそらく、問題が発生している理由は、e.Cancel == trueに設定した状態でApp_FormClosingを抜けてしまい、イベント(フォームを閉じる)がキャンセルされています。キャンセルしているのだから、当然フォームは閉じられません。

この問題は以下のコードで解決すると思います。

csharp

1void App_FormClosing(object sender, FormClosingEventArgs e) { 2 CancelTaskA(); 3}

CancelTaskA()が同期メソッドであるという前提です。App_FormClosingの処理が完了するまではフォームは閉じないので、複雑なことはせず、単純にTaskAのキャンセル処理を完了してください。


追記

CancellationTokenによるTaskの実行キャンセルは、あくまでC#のコード上でキャンセル処理を記述するものであり、何らかの処理(== OSコマンドによる重い処理)を途中で割り込み停止することはできません。最小単位では、TaskA内のwhile内の1行単位でキャンセルを行えることになります。その場合は記述はあまり難しくはないです。

というわけで、当初の目的であった「フォームを閉じる際、Taskをキャンセルしてから閉じる」部分のみを実現する場合は、以下のコードになります。

csharp

1// App_FormClosingは同期メソッドとする 2void App_FormClosing(object sender, FormClosingEventArgs e) { 3 CancelAllTask(); // フォームを閉じる時は、TaskAのキャンセルをリクエストする以上のことはしない 4} 5 6private CancellationTokenSource m_CTS = new CancellationTokenSource(); 7 8private Task TaskA() 9{ 10 return Task.Factory.StartNew(async () => 11 { 12 while (true) 13 { 14 await Task.Delay(2000, m_CTS.Token); // Task.DelayにCancellationTokenを渡すことで、待ち時間中にキャンセルされた場合、続きの実行は行われない 15 16 // 以下何らかの処理; 17 { 18 //もし、「何らかの処理」のコードの途中でキャンセル可能な部分がある場合はそこに以下の一行を挟む 19 m_CTS.Token.ThrowIfCancellationRequested(); 20 } 21 } 22 }, m_CTS.Token); 23} 24 25private void CancelAllTask() 26{ 27 m_CTS.Cancel(); 28 29 // もし、「何らかの処理」を割り込み停止する手段があるならここに記述 30} 31

以上のコードについて。
まず、「何らかの処理(== C#外の処理)」が既に実行中である場合、Taskのキャンセルでそれを停止することはできないので、1コマンド分の実行は諦めるしかないです。これを停止したいとなると、今度はOSコマンドのほうにキャンセル命令を投げる手段を改めて探さなければなりません。
そして、「何らかの処理」を停止できないのであれば、Taskの完全終了を確認する必要もなくなるので、キャンセルをリクエストしてフォームを閉じてしまえば良いです。TaskAはキャンセル可能な部分に入った瞬間にキャンセルされます。
ループ内でのキャンセル方法について、break;で抜けるとTaskのStatusがCompletedになりますが、この状況ではCanceledが得られることが望ましいので、ThrowIfCancellationRequestedを使ってキャンセルを行います。

本来は「何らかの処理」を含めてキャンセルしたいと思われますが、そちらのほうはTaskでは実現できません。
もし、「TaskAの「何らかの処理」が実行中ではなくなり、TaskAのキャンセルが完了するまでフォームを閉じたくない」なら、CancellAllTaskメソッド内に以下の一行を追加して下さい。

csharp

1taskA.ContinueWith(_ => { /* キャンセル完了時の処理 */ }).Wait();

投稿2016/08/05 01:54

編集2016/08/05 06:17
tamoto

総合スコア4103

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

matsu1

2016/08/05 04:17

ご質問の3点について、タスクAおよびそのキャンセル関数についてコードを追記しました。
tamoto

2016/08/05 04:33

ありがとうございます。回答を編集する前にいくつか確認させて下さい。 * CancelTaskA は CancelAllTask のことか? * 「フォームを閉じようとしたとき、TaskAを先に終了してからフォームを閉じたい」というだけの話だと思っていましたが、そのために何故複雑な非同期フローを導入しようと考えましたか? * TaskAが「完全に完了」してからフォームを閉じなければならない、その理由を教えて下さい。 以上3点について、情報提供をよろしくお願いします。
matsu1

2016/08/05 04:46

ご回答ありがとうございます。以下ご質問順に回答させて頂きます。 1.すみません。投稿時に関数名が誤っていました。CancelTaskA=CancelAllTaskです。 2.と3.タスクAで実行させる何らかの処理というのが具体的にはOSのコマンドを用いた大量コピーといったようなものでして、処理が重く常時行っておきたいもののため非同期タスクで実行させております。 また、閉じるボタンでアプリは落としたが、タスクAにより起動されたOSによる大量コピー処理がOSで実行され続けてしまう、といったような状態をなくしたいためです。 以上よろしくお願い致します。
tamoto

2016/08/05 05:02

ありがとうございます。だいぶ明確になってきました。 申し訳ないですが、もう少しだけ質問をさせて下さい。 以下の2つについて、こちらの認識に間違いないか、回答をお願いします。 * TaskA内では何らかの処理が2秒のSleepを挟んで繰り返し実行されているが、これは重い処理そのものを表現しているというわけではなく、実際に2秒おきに特定の処理を繰り返し実行している *「重いOSのコマンド」というのは、何らかのOSのコマンドを繰り返し発行しているという意味で、「ものすごく重い単発コマンド」を実行しているわけではない よろしくお願いします。
matsu1

2016/08/05 05:16

回答いたします。 1.そうですね。sleep自体は重い処理の表現ではありません。重い処理は2秒おきに繰返されるOSコマンドを用いたコピー処理です。 2.上記の重いOSコマンドとは、具体的にはOSを用いたコピー処理でして、以下のようなものです。  (1)copy src_folder dst_folder   (コピー元フォルダの全ファイルをコピー先に1コマンドでコピーする)  (2)copy src_folder/file1 dst_folder   (コピー元フォルダのファイル1をコピー先に1コマンドでコピーする) (1)であれば、フォルダ内のファイル数が万単位(合計数GB)のこともありますし (2)であれば、ファイルを1単位でコピーするという処理をファイル1,ファイル2…ファイルn個について逐次おこなうことも行っています。n=数万の場合もあります。 すなわち、単発で重い場合と、1個当たりは軽いがその数が膨大な場合と2種類行っております。 以上よろしくお願い致します。
tamoto

2016/08/05 06:20

現時点での情報を元に回答に追記しました。 質問や追加の情報提供があれば確認しますので、よろしくお願いします。
matsu1

2016/08/05 08:30

回答追記ありがとうございます。 おっしゃるようにタスクを1行単位でキャンセルするか、OSコマンド自体をキャンセルさせるかでアプローチが異なってきますね。 前者については、tamotoさんのわかりやすい解説でよく理解できました。ありがとうございます。 後者については、コピーコマンド自体をProcess.Start()で実行して、それをキャンセルしにいく方法で考えたいと思います。 また質問の機会ありましたら今後ともよろしくお願い致します。 ベストアンサーとさせて頂きます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問