web-dev-qa-db-ja.com

C#で「1回実行」時間遅延関数を作成する最良の方法

アクションとタイムアウトを受け取り、タイムアウト後にアクションを実行する関数を作成しようとしています。機能はノンブロッキングです。関数はスレッドセーフでなければなりません。私も本当に、本当にThread.Sleep()を避けたいです。

これまでのところ、私にできることはこれです:

long currentKey = 0;
ConcurrentDictionary<long, Timer> timers = new ConcurrentDictionary<long, Timer>();

protected void Execute(Action action, int timeout_ms)
{
    long currentKey = Interlocked.Increment(ref currentKey);
    Timer t = new Timer(
      (key) =>
         {
           action();
           Timer lTimer;
           if(timers.TryRemove((long)key, out lTimer))
           {
               lTimer.Dispose();
           }
         }, currentKey, Timeout.Infinite, Timeout.Infinite
      );

     timers[currentKey] = t;
     t.Change(timeout_ms, Timeout.Infinite);
}

問題は、コールバック自体からDispose()を呼び出すことは適切ではないということです。終了を「落とす」ことが安全かどうかはわかりません。つまり、ラムダが実行されている間はタイマーはライブであると見なされますが、そうであっても適切に破棄したいです。

「遅延を伴う1回の起動」は、System.Threadingにある他のライブラリのおそらく簡単な方法があるはずであるような一般的な問題のように思えますが、現時点で唯一考えられる解決策は、上記の間隔で実行される専用のクリーンアップタスクを使用します。何かアドバイス?

42
Chuu

使用しているC#のバージョンがわかりません。しかし、タスクライブラリを使用してこれを達成できると思います。そうすると、そのようなものになります。

public class PauseAndExecuter
{
    public async Task Execute(Action action, int timeoutInMilliseconds)
    {
        await Task.Delay(timeoutInMilliseconds);
        action();
    }
}
68
treze

これをうまく行うための.Net 4には何も組み込まれていません。 Thread.SleepまたはAutoResetEvent.WaitOne(timeout)は良くありません-それらはスレッドプールリソースを拘束します。

最も軽量なソリューションは、タイマーを使用することです-特に、多くのタスクを投げる場合。

まず、単純なスケジュールされたタスククラスを作成します。

class ScheduledTask
{
    internal readonly Action Action;
    internal System.Timers.Timer Timer;
    internal EventHandler TaskComplete;

    public ScheduledTask(Action action, int timeoutMs)
    {
        Action = action;
        Timer = new System.Timers.Timer() { Interval = timeoutMs };
        Timer.Elapsed += TimerElapsed;            
    }

    private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        Timer.Stop();
        Timer.Elapsed -= TimerElapsed;
        Timer = null;

        Action();
        TaskComplete(this, EventArgs.Empty);
    }
}

次に、スケジューラクラスを作成します。これも非常に簡単です。

class Scheduler
{        
    private readonly ConcurrentDictionary<Action, ScheduledTask> _scheduledTasks = new ConcurrentDictionary<Action, ScheduledTask>();

    public void Execute(Action action, int timeoutMs)
    {
        var task = new ScheduledTask(action, timeoutMs);
        task.TaskComplete += RemoveTask;
        _scheduledTasks.TryAdd(action, task);
        task.Timer.Start();
    }

    private void RemoveTask(object sender, EventArgs e)
    {
        var task = (ScheduledTask) sender;
        task.TaskComplete -= RemoveTask;
        ScheduledTask deleted;
        _scheduledTasks.TryRemove(task.Action, out deleted);
    }
}

次のように呼び出すことができ、非常に軽量です。

var scheduler = new Scheduler();

scheduler.Execute(() => MessageBox.Show("hi1"), 1000);
scheduler.Execute(() => MessageBox.Show("hi2"), 2000);
scheduler.Execute(() => MessageBox.Show("hi3"), 3000);
scheduler.Execute(() => MessageBox.Show("hi4"), 4000);
26
James Harcourt

私の例:

void startTimerOnce()
{
   Timer tmrOnce = new Timer();
   tmrOnce.Tick += tmrOnce_Tick;
   tmrOnce.Interval = 2000;
   tmrOnce.Start();
}

void tmrOnce_Tick(object sender, EventArgs e)
{
   //...
   ((Timer)sender).Dispose();
}
5
Nikolay Khilyuk

このメソッドを使用して、特定の時間にタスクをスケジュールします。

public void ScheduleExecute(Action action, DateTime ExecutionTime)
{
    Task WaitTask = Task.Delay(ExecutionTime.Subtract(DateTime.Now));
    WaitTask.ContinueWith(() => action());
    WaitTask.Start();
}

Int32の最大値のため、これは約24日間しか機能しないことに注意してください。

4
VoteCoffee

あなたが持っているモデルは、ワンショットタイマーを使用して、間違いなく行く方法です。確かに、それらのすべてに対して新しいスレッドを作成する必要はありません。単一のスレッドと、時間通りにキーが設定されたアクションの優先キューを持つことができますが、それは不必要な複雑さです。

コールバックでDisposeを呼び出すことは、おそらく良い考えではありませんが、試してみたいと思います。過去にこれを行ったことを思い出すようですが、うまくいきました。しかし、それはやるべきことの一種です、私は認めます。

コレクションからタイマーを削除するだけで、破棄することはできません。オブジェクトへの参照がない場合、ガベージコレクションの対象になります。つまり、Disposeメソッドwillがファイナライザによって呼び出されます。好きなだけタイムリーではありません。しかし、それは問題ではないはずです。ほんの少しの間ハンドルを漏らしているだけです。何千ものこれらのものが長い間処分されずに放置されていない限り、それは問題にはなりません。

別のオプションは、割り当てられたままで非アクティブ化されたタイマーのキューを持つことです(つまり、タイムアウトと間隔はTimeout.Infiniteに設定されます)。タイマーが必要な場合は、キューからタイマーを取得して設定し、コレクションに追加します。タイムアウトが経過すると、タイマーをクリアしてキューに戻します。必要に応じてキューを動的に拡大できます。必要に応じてキューを整えることもできます。

これにより、イベントごとに1つのタイマーがリークするのを防ぎます。代わりに、タイマーのプールがあります(スレッドプールに似ていますか?)。

2
Jim Mischel

時間の細かさをあまり気にしない場合は、1秒ごとに刻み、ThreadPoolのキューに入れる必要のある期限切れのアクションをチェックするタイマーを1つ作成できます。ストップウォッチクラスを使用して、タイムアウトを確認するだけです。

現在のアプローチを使用できますが、辞書にはキーとしてストップウォッチがあり、値としてアクションがあります。次に、すべてのKeyValuePairsを反復処理して、期限切れのストップウォッチを見つけ、アクションをキューに入れてから削除します。ただし、LinkedListの方がパフォーマンスとメモリ使用量が向上します(毎回すべてを列挙し、アイテムを削除する方が簡単だからです)。

2

ドキュメントには、System.Timers.Timerには、あなたが求めているものだけのために作成されたAutoResetプロパティがあることが明確に記載されています。

https://msdn.Microsoft.com/en-us/library/system.timers.timer.autoreset(v = vs.110).aspx

1
Lu4

trezeのコードは問題なく動作しています。これは、古い.NETバージョンを使用する必要のある人に役立つ場合があります。

private static volatile List<System.Threading.Timer> _timers = new List<System.Threading.Timer>();
private static object lockobj = new object();
public static void SetTimeout(Action action, int delayInMilliseconds)
{
    System.Threading.Timer timer = null;
    var cb = new System.Threading.TimerCallback((state) =>
    {
        lock (lockobj)
            _timers.Remove(timer);
        timer.Dispose();
        action();
    });
    lock (lockobj)
        _timers.Add(timer = new System.Threading.Timer(cb, null, delayInMilliseconds, System.Threading.Timeout.Infinite));
}
1
Koray

これは私のために働くようです。 15秒の遅延の後に_connection.Start()を呼び出すことができます。 -1ミリ秒のパラメーターは、繰り返さないことを示しています。

// Instance or static holder that won't get garbage collected (thanks chuu)
System.Threading.Timer t;

// Then when you need to delay something
var t = new System.Threading.Timer(o =>
            {
                _connection.Start(); 
            },
            null,
            TimeSpan.FromSeconds(15),
            TimeSpan.FromMilliseconds(-1));
1
naskew

MicrosoftのReactive Framework(NuGet "System.Reactive")を使用すると、次のことができます。

protected void Execute(Action action, int timeout_ms)
{
    Scheduler.Default.Schedule(TimeSpan.FromMilliseconds(timeout_ms), action);
}
1
Enigmativity

非同期アクションでアクションパラメーター自体を単純に呼び出してみませんか?

Action timeoutMethod = () =>
  {
       Thread.Sleep(timeout_ms);
       action();
  };

timeoutMethod.BeginInvoke();
0
Tejs