Лучший способ создать функцию с однократным запуском в C # - PullRequest
41 голосов
/ 06 мая 2011

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

Ответы [ 12 ]

67 голосов
/ 18 апреля 2013

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

public class PauseAndExecuter
{
    public async Task Execute(Action action, int timeoutInMilliseconds)
    {
        await Task.Delay(timeoutInMilliseconds);
        action();
    }
}
26 голосов
/ 18 апреля 2013

Нет ничего встроенного в .Net 4, чтобы сделать это красиво. Thread.Sleep или даже AutoResetEvent.WaitOne (тайм-аут) не годятся - они будут связывать ресурсы пула потоков, я был сожжен при попытке этого!

Самое легкое решение - использовать таймер, особенно если вам нужно будет выполнить много задач.

Сначала создайте простой класс запланированных задач:

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);
5 голосов
/ 18 июля 2015

Мой пример:

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 голосов
/ 08 января 2015

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

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

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

2 голосов
/ 18 апреля 2013

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

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

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

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

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

2 голосов
/ 06 мая 2011

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

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

1 голос
/ 30 октября 2018

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

protected void Execute(Action action, int timeout_ms)
{
    Scheduler.Default.Schedule(TimeSpan.FromMilliseconds(timeout_ms), action);
}
1 голос
/ 05 октября 2017

Кажется, это работает для меня.Это позволяет мне вызывать _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));
1 голос
/ 13 ноября 2016

В документации четко указано, что System.Timers.Timer имеет свойство AutoReset, созданное только для того, что вы просите:

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

1 голос
/ 26 октября 2016

код Трезе работает просто отлично.Это может помочь тем, кто должен использовать более старые версии .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));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...