69

X 分ごとにスレッドをディスパッチする必要がある C# プログラムがありますが、以前に (X 分から) 前にディスパッチされたスレッドがまだ実行されていない場合に限ります

普通の古いものTimerだけでは機能しません (以前にディスパッチされたプロセスがまだ終了しているかどうかに関係なく、X 分ごとにイベントをディスパッチするため)。

ディスパッチされるプロセスは、そのタスクを実行するのにかかる時間が大きく異なります。1 秒かかることもあれば、数時間かかることもあります。前回の開始時からまだ処理中の場合、プロセスを再度開始したくありません。

動作する C# サンプル コードを提供できる人はいますか?

4

16 に答える 16

60

私の意見では、この状況に移行する方法は、クラスを使用し、新しいスレッドをディスパッチする(またはディスパッチしない)たびにSystem.ComponentModel.BackgroundWorkerそのプロパティをチェックすることです。IsBusyコードは非常に単純です。次に例を示します。

class MyClass
{    
    private BackgroundWorker worker;

    public MyClass()
    {
        worker = new BackgroundWorker();
        worker.DoWork += worker_DoWork;
        Timer timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start();
    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if(!worker.IsBusy)
            worker.RunWorkerAsync();
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        //whatever You want the background thread to do...
    }
}

この例ではSystem.Timers.Timer、を使用しましたが、他のタイマーでも機能するはずです。このBackgroundWorkerクラスは、進行状況のレポートとキャンセルもサポートし、ディスパッチスレッドとの通信のイベント駆動型モデルを使用するため、揮発性変数などについて心配する必要はありません...

編集

キャンセルや進捗状況の報告など、より複雑な例を次に示します。

class MyClass
{    
    private BackgroundWorker worker;

    public MyClass()
    {
        worker = new BackgroundWorker()
        {
            WorkerSupportsCancellation = true,
            WorkerReportsProgress = true
        };
        worker.DoWork += worker_DoWork;
        worker.ProgressChanged += worker_ProgressChanged;
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;

        Timer timer = new Timer(1000);
        timer.Elapsed += timer_Elapsed;
        timer.Start();
    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        if(!worker.IsBusy)
            worker.RunWorkerAsync();
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker w = (BackgroundWorker)sender;

        while(/*condition*/)
        {
            //check if cancellation was requested
            if(w.CancellationPending)
            {
                //take any necessary action upon cancelling (rollback, etc.)

                //notify the RunWorkerCompleted event handler
                //that the operation was cancelled
                e.Cancel = true; 
                return;
            }

            //report progress; this method has an overload which can also take
            //custom object (usually representing state) as an argument
            w.ReportProgress(/*percentage*/);

            //do whatever You want the background thread to do...
        }
    }

    void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //display the progress using e.ProgressPercentage and/or e.UserState
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if(e.Cancelled)
        {
            //do something
        }
        else
        {
            //do something else
        }
    }
}

次に、それ以上の実行をキャンセルするには、を呼び出すだけworker.CancelAsync()です。これは完全にユーザーが処理するキャンセルメカニズムであることに注意してください(スレッドの中止など、すぐに使用できるものはサポートされていません)。

于 2013-01-30T00:17:57.027 に答える
23

あなたが求めたものを達成するために揮発性ブールを維持することができます:

private volatile bool _executing;

private void TimerElapsed(object state)
{
    if (_executing)
        return;

    _executing = true;

    try
    {
        // do the real work here
    }
    catch (Exception e)
    {
        // handle your error
    }
    finally
    {
        _executing = false;
    }
}
于 2012-12-02T17:19:09.817 に答える
10

経過したコールバックでタイマーを無効または有効にできます。

public void TimerElapsed(object sender, EventArgs e)
{
  _timer.Stop();

  //Do Work

  _timer.Start();
}
于 2012-09-24T17:59:43.577 に答える
7

データ/メソッドを処理する前に を使用してSystem.Threading.Timerを設定するだけで、完了したら次の呼び出しの準備ができて再起動できます。TimeoutInfiniteTimer

    private System.Threading.Timer _timerThread;
    private int _period = 2000;

    public MainWindow()
    {
        InitializeComponent();

        _timerThread = new System.Threading.Timer((o) =>
         {
             // Stop the timer;
             _timerThread.Change(-1, -1);

             // Process your data
             ProcessData();

             // start timer again (BeginTime, Interval)
             _timerThread.Change(_period, _period);
         }, null, 0, _period);
    }

    private void ProcessData()
    {
        // do stuff;
    }
于 2013-01-30T22:09:06.237 に答える
4

ここの投稿から PeriodicTaskFactory を使用する

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

Task task = PeriodicTaskFactory.Start(() =>
{
    Console.WriteLine(DateTime.Now);
    Thread.Sleep(5000);
}, intervalInMilliseconds: 1000, synchronous: true, cancelToken: cancellationTokenSource.Token);

Console.WriteLine("Press any key to stop iterations...");
Console.ReadKey(true);

cancellationTokenSource.Cancel();

Console.WriteLine("Waiting for the task to complete...");

Task.WaitAny(task);

以下の出力は、間隔が 1000 ミリ秒に設定されていても、タスク アクションの作業が完了するまで各反復が開始されないことを示しています。これは、synchronous: trueオプションのパラメーターを使用して実現されます。

Press any key to stop iterations...
9/6/2013 1:01:52 PM
9/6/2013 1:01:58 PM
9/6/2013 1:02:04 PM
9/6/2013 1:02:10 PM
9/6/2013 1:02:16 PM
Waiting for the task to complete...
Press any key to continue . . .

アップデート

PeriodicTaskFactory で「スキップされたイベント」の動作が必要な場合は、単に同期オプションを使用せず、Bob がここで行ったように Monitor.TryEnter を実装してください https://stackoverflow.com/a/18665948/222434

Task task = PeriodicTaskFactory.Start(() =>
{
    if (!Monitor.TryEnter(_locker)) { return; }  // Don't let  multiple threads in here at the same time.

    try
    {
        Console.WriteLine(DateTime.Now);
        Thread.Sleep(5000);
    }
    finally
    {
        Monitor.Exit(_locker);
    }

}, intervalInMilliseconds: 1000, synchronous: false, cancelToken: cancellationTokenSource.Token);

の良いところは、継続など、PeriodicTaskFactoryすべての TPL API で使用できる Task が返されることです。Task.Wait

于 2013-09-06T17:06:16.897 に答える
3

タスクの前にタイマーを停止し、タスクの完了後にタイマーを再開できます。これにより、テイクを一定の時間間隔で定期的に実行できます。

public void myTimer_Elapsed(object sender, EventArgs e)
{
    myTimer.Stop();
    // Do something you want here.
    myTimer.Start();
}
于 2012-09-24T18:00:33.543 に答える
3

タイマーのコールバックをバックグラウンド スレッドで起動する場合は、 System.Threading.Timerを使用できます。この Timer クラスを使用すると、「Timeout.Infinite定期的なシグナリングを無効にするように指定する」ことができます。コンストラクターの一部として、タイマーを 1 回だけ起動させます。

次に、最初のタイマーのコールバックが起動して完了するときに新しいタイマーを構築し、発生する準備が整うまで複数のタイマーがスケジュールされないようにすることができます。

ここでの利点は、一度に「次のイベント」以上のスケジュールを立てることがないため、タイマーを作成して繰り返しキャンセルする必要がないことです。

于 2012-09-24T18:06:18.833 に答える
3

この質問には、TPL のいくつかの機能に基づいた少し新しい回答を含め、すでに多くの適切な回答があります。しかし、私はここに不足を感じています:

  1. TPLベースのソリューションは、a)ここに完全に含まれているわけではなく、別の回答を参照しています.b)単一のメソッドでタイミングメカニズムを実装するためにasync/を使用する方法を示していません.c)参照されている実装はかなり複雑で、この特定の質問awaitの根底にある関連点がややわかりにくくなっています。
  2. ここでの元の質問は、目的の実装の特定のパラメーターについてややあいまいです (ただし、その一部はコメントで明確にされています)。同時に、他の読者も似たようなニーズを持っているかもしれませんが、同じではないかもしれません。
  3. コードを簡素化する方法のため、Taskand async/この方法を使用して定期的な動作を実装するのが特に好きです。await特にasync/await機能は、そうでなければ継続/コールバック実装の詳細によって分割されるコードを取得し、その自然で線形のロジックを単一のメソッドで保持するのに非常に役立ちます。しかし、その単純さを示す答えはここにはありません。

それで、その理論的根拠により、この質問にさらに別の答えを追加する動機が生まれました…


私にとって、最初に考慮すべきことは、「ここで正確にどのような動作が必要か?」ということです。ここでの質問は基本的な前提から始まります。タスクがタイマー期間よりも長くかかる場合でも、タイマーによって開始された期間タスクは同時に実行されるべきではないということです。ただし、次のような前提を満たす方法は複数あります。

  1. タスクの実行中にタイマーを実行しないでください。
  2. タイマーを実行します (これと、ここで提示する残りのオプションはすべて、タスクの実行中にタイマーが引き続き実行されることを前提としています)。前のタイマーティック。
  3. タイマーティックでのみタスクの実行を開始します。タスクがタイマー期間よりも長くかかる場合は、現在のタスクの実行中に新しいタスクを開始しないでください。また、現在のタスクが完了した後でも、次のタイマー ティックまで新しいタスクを開始しないでください。
  4. タスクがタイマー間隔よりも長くかかる場合は、タスクが完了した直後にタスクを再実行するだけでなく、タスクが「追いつく」まで必要な回数だけ実行します。つまり、時間の経過とともに、タイマー ティックごとに1 回タスクを実行するように最善を尽くします。

コメントに基づいて、#3 オプションが OP の元の要求に最もよく一致するという印象がありますが、#1 オプションも機能する可能性があるようです。しかし、オプション #2 と #4 は他の誰かよりも好ましいかもしれません。

次のコード例では、これらのオプションを 5 つの異なる方法で実装しています (そのうちの 2 つはオプション #3 を実装していますが、方法が少し異なります)。もちろん、必要に応じて適切な実装を選択します。1 つのプログラムで 5 つすべてが必要なわけではありません。:)

重要な点は、これらの実装のすべてにおいて、自然にかつ非常に単純な方法で、タスクをピリオドではあるが同時実行ではない方法で実行することです。つまり、タイマーベースの実行モデルを効果的に実装しながら、質問の主要な要求ごとに、タスクが一度に 1 つのスレッドによってのみ実行されるようにします。

この例ではCancellationTokenSource、 を使用して period タスクを中断し、 を利用しawaitて例外ベースのモデルをクリーンでシンプルな方法で処理する方法も示しています。

class Program
{
    const int timerSeconds = 5, actionMinSeconds = 1, actionMaxSeconds = 7;

    static Random _rnd = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to interrupt timer and exit...");
        Console.WriteLine();

        CancellationTokenSource cancelSource = new CancellationTokenSource();

        new Thread(() => CancelOnInput(cancelSource)).Start();

        Console.WriteLine(
            "Starting at {0:HH:mm:ss.f}, timer interval is {1} seconds",
            DateTime.Now, timerSeconds);
        Console.WriteLine();
        Console.WriteLine();

        // NOTE: the call to Wait() is for the purpose of this
        // specific demonstration in a console program. One does
        // not normally use a blocking wait like this for asynchronous
        // operations.

        // Specify the specific implementation to test by providing the method
        // name as the second argument.
        RunTimer(cancelSource.Token, M1).Wait();
    }

    static async Task RunTimer(
        CancellationToken cancelToken, Func<Action, TimeSpan, Task> timerMethod)
    {
        Console.WriteLine("Testing method {0}()", timerMethod.Method.Name);
        Console.WriteLine();

        try
        {
            await timerMethod(() =>
            {
                cancelToken.ThrowIfCancellationRequested();
                DummyAction();
            }, TimeSpan.FromSeconds(timerSeconds));
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine();
            Console.WriteLine("Operation cancelled");
        }
    }

    static void CancelOnInput(CancellationTokenSource cancelSource)
    {
        Console.ReadKey();
        cancelSource.Cancel();
    }

    static void DummyAction()
    {
        int duration = _rnd.Next(actionMinSeconds, actionMaxSeconds + 1);

        Console.WriteLine("dummy action: {0} seconds", duration);
        Console.Write("    start: {0:HH:mm:ss.f}", DateTime.Now);
        Thread.Sleep(TimeSpan.FromSeconds(duration));
        Console.WriteLine(" - end: {0:HH:mm:ss.f}", DateTime.Now);
    }

    static async Task M1(Action taskAction, TimeSpan timer)
    {
        // Most basic: always wait specified duration between
        // each execution of taskAction
        while (true)
        {
            await Task.Delay(timer);
            await Task.Run(() => taskAction());
        }
    }

    static async Task M2(Action taskAction, TimeSpan timer)
    {
        // Simple: wait for specified interval, minus the duration of
        // the execution of taskAction. Run taskAction immediately if
        // the previous execution too longer than timer.

        TimeSpan remainingDelay = timer;

        while (true)
        {
            if (remainingDelay > TimeSpan.Zero)
            {
                await Task.Delay(remainingDelay);
            }

            Stopwatch sw = Stopwatch.StartNew();
            await Task.Run(() => taskAction());
            remainingDelay = timer - sw.Elapsed;
        }
    }

    static async Task M3a(Action taskAction, TimeSpan timer)
    {
        // More complicated: only start action on time intervals that
        // are multiples of the specified timer interval. If execution
        // of taskAction takes longer than the specified timer interval,
        // wait until next multiple.

        // NOTE: this implementation may drift over time relative to the
        // initial start time, as it considers only the time for the executed
        // action and there is a small amount of overhead in the loop. See
        // M3b() for an implementation that always executes on multiples of
        // the interval relative to the original start time.

        TimeSpan remainingDelay = timer;

        while (true)
        {
            await Task.Delay(remainingDelay);

            Stopwatch sw = Stopwatch.StartNew();
            await Task.Run(() => taskAction());

            long remainder = sw.Elapsed.Ticks % timer.Ticks;

            remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
        }
    }

    static async Task M3b(Action taskAction, TimeSpan timer)
    {
        // More complicated: only start action on time intervals that
        // are multiples of the specified timer interval. If execution
        // of taskAction takes longer than the specified timer interval,
        // wait until next multiple.

        // NOTE: this implementation computes the intervals based on the
        // original start time of the loop, and thus will not drift over
        // time (not counting any drift that exists in the computer's clock
        // itself).

        TimeSpan remainingDelay = timer;
        Stopwatch swTotal = Stopwatch.StartNew();

        while (true)
        {
            await Task.Delay(remainingDelay);
            await Task.Run(() => taskAction());

            long remainder = swTotal.Elapsed.Ticks % timer.Ticks;

            remainingDelay = TimeSpan.FromTicks(timer.Ticks - remainder);
        }
    }

    static async Task M4(Action taskAction, TimeSpan timer)
    {
        // More complicated: this implementation is very different from
        // the others, in that while each execution of the task action
        // is serialized, they are effectively queued. In all of the others,
        // if the task is executing when a timer tick would have happened,
        // the execution for that tick is simply ignored. But here, each time
        // the timer would have ticked, the task action will be executed.
        //
        // If the task action takes longer than the timer for an extended
        // period of time, it will repeatedly execute. If and when it
        // "catches up" (which it can do only if it then eventually
        // executes more quickly than the timer period for some number
        // of iterations), it reverts to the "execute on a fixed
        // interval" behavior.

        TimeSpan nextTick = timer;
        Stopwatch swTotal = Stopwatch.StartNew();

        while (true)
        {
            TimeSpan remainingDelay = nextTick - swTotal.Elapsed;

            if (remainingDelay > TimeSpan.Zero)
            {
                await Task.Delay(remainingDelay);
            }

            await Task.Run(() => taskAction());
            nextTick += timer;
        }
    }
}

最後に 1 つ: 別の質問の複製としてこの Q&A をたどった後、この Q&A に出会いました。その他の質問では、こことは異なり、OP はSystem.Windows.Forms.Timerクラスを使用していることを特に指摘していました。もちろん、このクラスが主に使用されるのはTick、UI スレッドでイベントが発生するという優れた機能があるためです。

さて、it と this の両方の質問には、バックグラウンド スレッドで実際に実行されるタスクが含まれているため、そのタイマー クラスの UI スレッド アフィニティ動作は、これらのシナリオでは特に使用されません。ここでのコードは、「バックグラウンド タスクを開始する」というパラダイムに一致するように実装されていますが、taskActionデリゲートが実行されて待機するのではなく、単に直接呼び出されるように簡単に変更できますTask。上記の構造上の利点に加えて、async/を使用する利点は、クラスに望ましいスレッド アフィニティの動作が維持されることです。awaitSystem.Windows.Forms.Timer

于 2015-05-15T07:59:25.970 に答える
3

これを実現するには、タイマーとセマフォの使用から、揮発性変数の使用、TPL の使用、Quartz などのオープンソース スケジューリング ツールの使用まで、少なくとも 20 の異なる方法があります。

スレッドの作成はコストのかかる作業です。そのため、スレッドを 1 つだけ作成してバックグラウンドで実行したままにしない理由はありません。スレッドはほとんどの時間を IDLE で過ごすため、システムに実際の負荷がかかることはありません。定期的に起きて仕事をしてから、一定時間眠りにつく。タスクにどれだけ時間がかかっても、完了後、新しいタスクを開始する前に、常に少なくとも「waitForWork」期間待機します。

    //wait 5 seconds for testing purposes
    static TimeSpan waitForWork = new TimeSpan(0, 0, 0, 5, 0);
    static ManualResetEventSlim shutdownEvent = new ManualResetEventSlim(false);
    static void Main(string[] args)
    {
        System.Threading.Thread thread = new Thread(DoWork);
        thread.Name = "My Worker Thread, Dude";
        thread.Start();

        Console.ReadLine();
        shutdownEvent.Set();
        thread.Join();
    }

    public static void DoWork()
    {
        do
        {
            //wait for work timeout or shudown event notification
            shutdownEvent.Wait(waitForWork);

            //if shutting down, exit the thread
            if(shutdownEvent.IsSet)
                return;

            //TODO: Do Work here


        } while (true);

    }
于 2013-01-28T21:37:37.080 に答える
3

System.Threading.Timer を使用できます。トリックは、初期時間のみを設定することです。最初の時間は、前の間隔が終了したとき、またはジョブが終了したときに再度設定されます (これは、ジョブが間隔よりも長くかかっている場合に発生します)。これがサンプルコードです。

class Program
{


    static System.Threading.Timer timer;
    static bool workAvailable = false;
    static int timeInMs = 5000;
    static object o = new object(); 

    static void Main(string[] args)
    {
        timer = new Timer((o) =>
            {
                try
                {
                    if (workAvailable)
                    {
                        // do the work,   whatever is required.
                        // if another thread is started use Thread.Join to wait for the thread to finish
                    }
                }
                catch (Exception)
                {
                    // handle
                }
                finally
                {
                    // only set the initial time, do not set the recurring time
                    timer.Change(timeInMs, Timeout.Infinite);
                }
            });

        // only set the initial time, do not set the recurring time
        timer.Change(timeInMs, Timeout.Infinite);
    }
于 2013-01-31T06:15:57.547 に答える
1

これはあなたが望むことをするはずです。スレッドを実行し、終了するまでスレッドに参加します。タイマーループに入り、スレッドが時期尚早に実行されていないことを確認してから、再びオフになって実行します。

using System.Threading;

public class MyThread
{
    public void ThreadFunc()
    {
        // do nothing apart from sleep a bit
        System.Console.WriteLine("In Timer Function!");
        Thread.Sleep(new TimeSpan(0, 0, 5));
    }
};

class Program
{
    static void Main(string[] args)
    {
        bool bExit = false;
        DateTime tmeLastExecuted;

        // while we don't have a condition to exit the thread loop
        while (!bExit)
        {
            // create a new instance of our thread class and ThreadStart paramter
            MyThread myThreadClass = new MyThread();
            Thread newThread = new Thread(new ThreadStart(myThreadClass.ThreadFunc));

            // just as well join the thread until it exits
            tmeLastExecuted = DateTime.Now; // update timing flag
            newThread.Start();
            newThread.Join();

            // when we are in the timing threshold to execute a new thread, we can exit
            // this loop
            System.Console.WriteLine("Sleeping for a bit!");

            // only allowed to execute a thread every 10 seconds minimum
            while (DateTime.Now - tmeLastExecuted < new TimeSpan(0, 0, 10));
            {
                Thread.Sleep(100); // sleep to make sure program has no tight loops
            }

            System.Console.WriteLine("Ok, going in for another thread creation!");
        }
    }
}

次のようなものを生成する必要があります:

タイマー機能で!少し寝て!OK、別のスレッド作成に行きます!タイマー機能で!少し寝て!OK、別のスレッド作成に行きます!タイマー機能で!.....。

お役に立てれば!SR

于 2012-09-24T18:32:05.030 に答える
1

これの根性はExecuteTaskCallback方法です。このビットは何らかの作業を行うように課されますが、まだ行っていない場合に限ります。このために、メソッドで最初に通知されるように設定されているManualResetEvent( ) を使用しました。canExecuteStartTaskCallbacks

の使い方に注意してくださいcanExecute.WaitOne(0)。ゼロは、 ( MSDNWaitOne )の状態ですぐに戻ることを意味します。ゼロを省略すると、最終的にタスクを実行するためのすべての呼び出しが行われることになり、かなり悲惨な結果になる可能性があります。WaitHandleExecuteTaskCallback

もう 1 つの重要なことは、処理をきれいに終了できることです。他の作業が進行中の可能性がある場合は実行する方が望ましいと思われるため、 でそれTimer以上のメソッドを実行しないように選択しました。StopTaskCallbacksこれにより、新しい作業が行われないこととcanExecute.WaitOne();、最後のタスクがある場合は への後続の呼び出しが実際に最後のタスクをカバーすることの両方が保証されます。

private static void ExecuteTaskCallback(object state)
{
    ManualResetEvent canExecute = (ManualResetEvent)state;

    if (canExecute.WaitOne(0))
    {
        canExecute.Reset();
        Console.WriteLine("Doing some work...");
        //Simulate doing work.
        Thread.Sleep(3000);
        Console.WriteLine("...work completed");
        canExecute.Set();
    }
    else
    {
        Console.WriteLine("Returning as method is already running");
    }
}

private static void StartTaskCallbacks()
{
    ManualResetEvent canExecute = new ManualResetEvent(true),
        stopRunning = new ManualResetEvent(false);
    int interval = 1000;

    //Periodic invocations. Begins immediately.
    Timer timer = new Timer(ExecuteTaskCallback, canExecute, 0, interval);

    //Simulate being stopped.
    Timer stopTimer = new Timer(StopTaskCallbacks, new object[]
    {
        canExecute, stopRunning, timer
    }, 10000, Timeout.Infinite);

    stopRunning.WaitOne();

    //Clean up.
    timer.Dispose();
    stopTimer.Dispose();
}

private static void StopTaskCallbacks(object state)
{
    object[] stateArray = (object[])state;
    ManualResetEvent canExecute = (ManualResetEvent)stateArray[0];
    ManualResetEvent stopRunning = (ManualResetEvent)stateArray[1];
    Timer timer = (Timer)stateArray[2];

    //Stop the periodic invocations.
    timer.Change(Timeout.Infinite, Timeout.Infinite);

    Console.WriteLine("Waiting for existing work to complete");
    canExecute.WaitOne();
    stopRunning.Set();
}
于 2013-01-26T10:03:36.803 に答える