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

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

ただいまの
回答率

90.51%

  • C#

    9012questions

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

  • 非同期処理

    134questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

先に実行されているタスクがあればキャンセルして終了を待ってから実行する

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 777

twck

score 291

 前提・実現したいこと

非同期で実行したいタスクがありますが、そのタスクは同時に複数動かしたくありません。
先に実行されているタスクがあった場合は、そのタスクをキャンセルして、終了を待ってから新しいタスクを実行したいです。

以下のように作ってみて一応動作しているものの、非同期処理の知識が無いためこの作り方であっているのか自信がありません。
もっと適した作り方があると思うのですが、皆さんならどう作られますか?

 該当のソースコード

private int taskNo = 0;

private List<CancellationTokenSource> cancelTokenSources = new List<CancellationTokenSource>();

private async void button1_Click(object sender, EventArgs e)
{
    var no = this.taskNo++;

    CancellationTokenSource cts = null;
    try
    {
        // 自身の CancellationTokenSource を生成してリストに登録
        lock (cancelTokenSources)
        {
            cts = new CancellationTokenSource();
            cancelTokenSources.Add(cts);
        }

        // 先に実行しているタスクが終了するまで待機するループ
        while (true)
        {
            lock (cancelTokenSources)
            {
                // 最後に登録されたCancellationTokenSource以外はキャンセルする
                for (var i = 0; i < (cancelTokenSources.Count - 1); i++)
                {
                    cancelTokenSources[i].Cancel();
                }

                // キャンセル途中のCancellationTokenSourceがなければループを抜ける
                if (cancelTokenSources.Count(n => n.IsCancellationRequested) <= 0) { break; }
            }

            // 自身がキャンセルされた場合はループを抜ける
            if (cts.IsCancellationRequested) { break; }

            await Task.Delay(100);
        };

        // 自身がキャンセルされていなければタスクを実行する
        // 自身がキャンセルされている場合はタスクの生成すらしない
        if (cts.IsCancellationRequested == false)
        {
            int r = await Task1(cts.Token, no);
        }
    }
    finally
    {
        // 自身の CancellationTokenSource をリストから削除して廃棄
        if (cts != null)
        {
            lock (cancelTokenSources)
            {
                cancelTokenSources.Remove(cts);
                cts.Dispose();
            }
        }
    }
}

// 非同期だが同時に複数実行したくないタスク(内容に意味はない)
private async Task<int> Task1(CancellationToken token, int no)
{
    Console.WriteLine(no + ":start");
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(no + ":" + i);
        if (token.IsCancellationRequested)
        {
            Console.WriteLine(no + ":cancel");
            return -1;
        }
        await Task.Delay(1000);
    }
    Console.WriteLine(no + ":end");
    return 1;
}

 補足情報(FW/ツールのバージョンなど)

Visual Studio Community 2017
.NET Framework 4.7

 追記

実行しやすいようにと、私が不用意にサンプルコードをフォームアプリケーションで書いてしまったために、ユーザーによる操作を想定していると誤解させてしまいました。
実際には button1_Clickメソッドの部分は他のプログラムから非常に素早く連続実行される可能性も想定しています。
極端に言えば以下のように並列実行されても問題なく4つがキャンセルされて、最後の1つだけ実行が続く感じです。

Parallel.Invoke(
    () => button1_Click(null, null),
    () => button1_Click(null, null),
    () => button1_Click(null, null),
    () => button1_Click(null, null),
    () => button1_Click(null, null)
);
  • 気になる質問をクリップする

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

みなさんならどうしますかということなので、私なら別のクラスに分離します。
コメント欄にて指摘いただいたので書き直しました。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private int taskId = 0;

        private void button1_Click(object sender, EventArgs e)
        {
            for (int j = 0; j < 10; j++)
            {
                Something.DoSomething(async ct =>
                {
                    taskId++;
                    for (int i = 0; i < 100; i++)
                    {
                        if (ct.IsCancellationRequested) break;
                        Debug.WriteLine($"{taskId}:{i}:1");
                        await Task.Delay(100);
                        Debug.WriteLine($"{taskId}:{i}:2");
                    }
                });
            }
        }
    }

    static class Something
    {
        static CancellationTokenSource cts;
        static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
        static object lockObject = new object();

        public static Task DoSomething(Func<CancellationToken, Task> action)
        {
            CancellationToken token;
            lock (lockObject)
            {
                Cancel();
                cts = new CancellationTokenSource();
                token = cts.Token;
            }
            return Task.Run(async () =>
            {
                await semaphore.WaitAsync();
                try
                {
                    await action(token);
                }
                finally
                {
                    semaphore.Release();
                }
            });
        }

        public static void Cancel()
        {
            cts?.Cancel();
        }
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/16 19:55

    横から失礼します。ほぼ同じ実装を考えました。が、このコードのセマフォは機能しなさそうなので DoSomething 全体を lock した方がいいと思います。

    キャンセル

  • 2018/10/16 20:35

    本当ですね。ありがとうございました。
    修正しました。

    キャンセル

  • 2018/10/18 10:37

    なるほど。SemaphoreSlimクラスで共有資源の管理ができるのですね。
    参考にいたします。

    キャンセル

  • 2018/10/18 12:42

    buttonコントロールのclickイベントとしてユーザーの連打くらいなら問題なく動くと思うのですが、もし、button1_Clickを並列で2つ連続実行すると以下のような順番で処理される可能性がありませんか?
    1番目の cts?.Cancel();
    2番目の cts?.Cancel();
    1番目の cts = new CancellationTokenSource();
    2番目の cts = new CancellationTokenSource();
    こうなると2番目が1番目をキャンセルできずに semaphore.WaitAsync(); で待たされて、しばらく後に3番目が起動されても cts の中は2番目のCancellationTokenSourceなので1番目をキャンセルできないとかありませんか?

    私がサンプルコードとして起動部分をbuttonコントロールのclickイベントとして書いてしまったのが悪いんですけど。

    キャンセル

  • 2018/10/18 12:49

    ユーザーはそこまで素早く連打できないので省略しました。
    必要であれば Cancel 以降 3 行をロックしてください。

    キャンセル

  • 2018/10/18 13:12

    ああ、やはりユーザーの操作を想定してのコードですよね。
    へたにサンプルコードを書かなければよかった。
    でも Cancel 直後からロックしても以下のようになる可能性がごくわずかに残るので、そのときは2番目は1番目をキャンセルできませんよね。
    1番目の cts?.Cancel();
    2番目の cts?.Cancel(); cts == null なので Cancelは起こらない
    1番目の semaphore.WaitAsync(); ロックして進む
    2番目の semaphore.WaitAsync(); ロックされているので止まる

    とりあえず SemaphoreSlimクラス を教えていただいたので、これで自分でも考えてみます。

    キャンセル

  • 2018/10/18 13:13

    直後ではなく、Cancel を含めたロックを意図しています。
    ですから、現在の SemaphoreSlim と同じインスタンスは使えません。

    キャンセル

  • 2018/10/18 13:34

    ああ、SemaphoreSlimとは別のロック用オブジェクトを用意するということですね。
    あと「Cancel 以降 3 行」と書いてあったので、Cancel(); と await semaphore.WaitAsync(); のあいだの以下の3行のことだと思ってCancel();は含めないのかと思ってしまいました。申し訳ありません。
    cts = new CancellationTokenSource();
    var token = cts.Token;
    return Task.Run(async () =>

    キャンセル

  • 2018/10/18 13:36

    書き直しました。

    キャンセル

0

その非同期実行したいタスクのコードに、定期的にキャンセルをチェックして、キャンセルをされたら実行を終了するようなコードを組んでおきます
そして、あとに実行するコードの方に、キャンセルを通知して、実行終了を待つコードを組んでおきます

まあしかし、一つだけしか実行したくない、と言うならわざわざ別タスクにする必要はないのでは。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/16 13:20 編集

    メインスレッドとは非同期で実行する必要があるので別タスクにする必要があります。
    提示したソースコードをご覧いただければ分かると思いますが、定期的なキャンセルのチェックと終了はCancellationTokenSourceを使用して出来ていると思います。
    でも「その非同期実行したいタスクのコードに、定期的にキャンセルをチェックして、キャンセルをされたら実行を終了するようなコードを組んでおきます」とわざわざ御指摘なさってるということは、他にもっと良い方法があるのでしょうか?
    あと「実行するコードの方に、キャンセルを通知して、実行終了を待つコード」というのを具体的なコードとして教えていただけないでしょうか?

    キャンセル

  • 2018/10/16 14:18 編集

    そういうことなら、別タスクで実行したルーチン内で各関数を順番に実行していけば事が足りるのでは。
    #そうすればタスク間でのゴタゴタは気にする必要がない

    提示されたコードですが、あんまし詳しく見てませんが、
    ・cancelTokenSourcesはlistにする必要はあるのか
     task1が実行中、ってことはbutton1_Clickも実行中ってことなんで、これだけみてればいい?
    ・実行終了待ちに単純ループだとCPUが無駄。autoreseteventとか使えばどうでしょ
    ・lockには独立したオブジェクトを指定しよう
    ・await Task.Delay(1000); → Thread.sleep(1000);
    ぐらいでしょうか。

    キャンセル

  • 2018/10/16 20:09

    twckさん、y_waiwaiさん、横から失礼します。
    ・await Task.Delay(1000); → Thread.sleep(1000); というご指摘ですが、これは、何か処理をしているからawaitでなくスレッドを止めたほうがそれらしい という理解であっていますか?

    キャンセル

  • 2018/10/18 10:42 編集

    >cancelTokenSourcesはlistにする必要はあるのか
    質問用のコードとして簡単に動作するようにbuttonコントロールのイベントで作ってあるのでアレなのですが、実際には button1_Clickの部分は並列実行されることもあるとしてください。
    なので button1_Click を3つ以上同時に実行された場合、listにしてあるほうが都合が良かったのです。

    >実行終了待ちに単純ループだとCPUが無駄。autoreseteventとか使えばどうでしょ
    なるほど。シグナル処理というものがあるのですね。調べてやってみます。

    >lockには独立したオブジェクトを指定しよう
    すみません。質問用のコードとして要素を少なくしようとしたため、独立したlockオブジェクトは作りませんでした。

    >await Task.Delay(1000); → Thread.sleep(1000);
    >何か処理をしているからawaitでなくスレッドを止めたほうがそれらしい という理解であっていますか?
    そうです。質問用のコードとして動かしたときにそれらしくスレッドを止めたかっただけなので、本当はDelayもSleepもしたいわけではなく、実際には重い処理が入るだけです。

    キャンセル

同じタグがついた質問を見る

  • C#

    9012questions

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

  • 非同期処理

    134questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。