Подтвердить что ты не робот

Лучший способ создать функцию задержки с задержкой "один раз" в С#

Я пытаюсь создать функцию, которая принимает действие и время ожидания, и выполняет действие после таймаута. Функция должна быть неблокирующей. Функция должна быть потокобезопасной. Я также очень хочу избавиться от 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() из самого обратного вызова не может быть хорошим. Я не уверен, что безопасно "упасть" с конца, т.е. Таймеры считаются живыми, пока их лямбды выполняются, но даже если это так, я бы предпочел правильно распоряжаться им.

"Огонь один раз с задержкой" кажется такой общей проблемой, что должен быть простой способ сделать это, возможно, в какой-то другой библиотеке в System.Threading я пропал без вести, но сейчас единственное решение, которое я могу придумать является модификацией вышеописанной задачи очистки, выполняемой с интервалом. Любые советы?

4b9b3361

Ответ 1

Я не знаю, какую версию С# вы используете. Но я думаю, вы могли бы это сделать, используя библиотеку задач. Тогда это выглядело бы так.

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

Ответ 2

Нет ничего встроенного в .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);

Ответ 3

Мой пример:

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();
}

Ответ 4

Я использую этот метод, чтобы запланировать задачу на определенное время:

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

Следует отметить, что это работает только около 24 дней из-за максимального значения int32.

Ответ 5

Если вы не заботитесь о гранулярности времени, вы можете создать один таймер, который будет отмечать каждую секунду, и проверяет на действия с истекшим сроком действия, которые должны быть поставлены в очередь на ThreadPool. Просто используйте класс секундомера, чтобы проверить время ожидания.

Вы можете использовать свой текущий подход, за исключением того, что ваш словарь будет иметь секундомер в качестве его ключа и действия в качестве его значения. Затем вы просто переходите на все KeyValuePairs и находите секундомер, который истекает, очередь в Action, а затем удалите его. Однако вы получите лучшую производительность и использование памяти из LinkedList (так как вы будете перечислять все это каждый раз и проще удалить элемент).

Ответ 6

Модель, которую вы используете, используя таймер с одним выстрелом, определенно является способом выхода. Вы, конечно же, не хотите создавать новый поток для каждого из них. У вас может быть один поток и приоритетная очередь действий, введенных во времени, но эта ненужная сложность.

Вызов Dispose в обратном вызове, вероятно, не является хорошей идеей, хотя у меня возникнет соблазн попробовать. Кажется, я вспоминаю об этом в прошлом, и все получилось хорошо. Я признаю, что это какая-то призрачная вещь.

Вы можете просто удалить таймер из коллекции и не утилизировать его. Без ссылок на объект он будет иметь право на сбор мусора, а это означает, что метод Dispose вызывается финализатором. Просто не так своевременно, как вам может понравиться. Но это не должно быть проблемой. Вы на короткое время просачиваете ручку. До тех пор, пока у вас не будет тысячи этих вещей, которые будут сидеть без проблем в течение длительного периода, это не будет проблемой.

Другой вариант - иметь очередь таймеров, которая остается выделенной, но деактивирована (т.е. их тайм-аут и интервалы установлены на Timeout.Infinite). Когда вам нужен таймер, вы вытаскиваете его из очереди, устанавливаете и добавляете в свою коллекцию. По истечении таймаута вы очищаете таймер и помещаете его обратно в очередь. Вы можете динамически вырастить очередь, если вам нужно, и вы могли бы время от времени ухаживать за ней.

Это предотвратит утечку одного таймера для каждого события. Вместо этого у вас будет пул таймеров (как и пул потоков, нет?).

Ответ 7

Код треза работает нормально. Это может помочь тем, кто должен использовать более старые версии .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));
}

Ответ 9

Кажется, это работает для меня. Это позволяет мне вызывать _connection.Start() после 15-секундной задержки. Параметр -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));

Ответ 10

Используйте Microsoft Reactive Framework (NuGet "System.Reactive"), и тогда вы можете сделать это:

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

Ответ 11

Почему бы просто не вызвать сам свой параметр действия в асинхронном действии?

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

timeoutMethod.BeginInvoke();

Ответ 12

Task.Delay(timeoutInMilliseconds).ContinueWith(t => yourAction());

Ответ 13

Это может быть немного поздно, но вот решение, которое я сейчас использую для обработки отложенного выполнения:

public class OneShotTimer
{

    private volatile readonly Action _callback;
    private OneShotTimer(Action callback, long msTime)
    {
        _callback = callback;
        var timer = new Threading.Timer(TimerProc);
        timer.Change(msTime, Threading.Timeout.Infinite);
    }

    private void TimerProc(object state)
    {
        try {
            // The state object is the Timer object. 
            ((Threading.Timer)state).Dispose();
            _callback.Invoke();
        } catch (Exception ex) {
            // Handle unhandled exceptions
        }
    }

    public static OneShotTimer Start(Action callback, TimeSpan time)
    {
        return new OneShotTimer(callback, Convert.ToInt64(time.TotalMilliseconds));
    }
    public static OneShotTimer Start(Action callback, long msTime)
    {
        return new OneShotTimer(callback, msTime);
    }
}

Вы можете использовать это так:

OneShotTimer.Start(() => DoStuff(), TimeSpan.FromSeconds(1))