Llamadas de función retrasadas

92

¿Existe un método simple y agradable para retrasar una llamada a una función mientras se deja que el hilo continúe ejecutándose?

p.ej

public void foo()
{
    // Do stuff!

    // Delayed call to bar() after x number of ms

    // Do more Stuff
}

public void bar()
{
    // Only execute once foo has finished
}

Soy consciente de que esto se puede lograr mediante el uso de un temporizador y controladores de eventos, pero me preguntaba si existe una forma estándar de C # para lograrlo.

Si alguien tiene curiosidad, la razón por la que esto es necesario es que foo () y bar () están en clases diferentes (singleton) que necesito llamar entre sí en circunstancias excepcionales. El problema es que esto se hace en la inicialización, por lo que foo necesita llamar a bar, que necesita una instancia de la clase foo que se está creando ... de ahí la llamada retardada a bar () para garantizar que foo esté completamente instanciado. ¡Casi huele a mal diseño!

EDITAR

¡Tomaré en cuenta los puntos sobre el mal diseño! Durante mucho tiempo pensé que podría mejorar el sistema, sin embargo, esta desagradable situación solo ocurre cuando se lanza una excepción, en todas las demás ocasiones los dos singleton coexisten muy bien. Creo que no voy a dar vueltas con patrones asincrónicos desagradables, sino que voy a refactorizar la inicialización de una de las clases.

TK.
fuente
Debe solucionarlo pero no mediante el uso de subprocesos (o cualquier otra práctica asíncrona para el caso)
ShuggyCoUk
1
Tener que usar subprocesos para sincronizar la inicialización de objetos es la señal de que debe tomar otro camino. Orchestrator parece una mejor opción.
thinkbeforecoding
1
¡Resurrección! - Al comentar sobre el diseño, puede optar por tener una inicialización en dos etapas. A partir de la API de Unity3D, hay Awakey Startfases. En la Awakefase, se configura usted mismo, y al final de esta fase se inicializan todos los objetos. Durante la Startfase, los objetos pueden comenzar a comunicarse entre sí.
cod3monk3y
1
La respuesta aceptada debe cambiarse
Brian Webster

Respuestas:

178

Gracias al moderno C # 5/6 :)

public void foo()
{
    Task.Delay(1000).ContinueWith(t=> bar());
}

public void bar()
{
    // do stuff
}
Korayem
fuente
15
Esta respuesta es asombrosa por 2 razones. La simplicidad del código y el hecho de que Delay NO crea un hilo ni usa el grupo de hilos como otros Task.Run o Task.StartNew ... es internamente un temporizador.
Zyo
Una solución decente.
x4h1d
6
También tenga en cuenta una versión equivalente ligeramente más limpia (IMO): Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran
5
@Zyo En realidad, usa un hilo diferente. Intente acceder a un elemento de la interfaz de usuario desde él y activará una excepción.
TudorT
@TudorT - Si Zyo tiene razón en que se ejecuta en un subproceso ya existente que ejecuta eventos de temporizador, entonces su punto es que no consume recursos adicionales creando un nuevo subproceso, ni haciendo cola para el grupo de subprocesos. (Aunque no sé si crear un temporizador es significativamente más barato que poner en cola una tarea para el grupo de subprocesos, que TAMBIÉN no crea un subproceso, ese es el objetivo del grupo de subprocesos).
ToolmakerSteve
96

Yo mismo he estado buscando algo como esto: se me ocurrió lo siguiente, aunque usa un temporizador, solo lo usa una vez para el retraso inicial y no requiere ninguna Sleepllamada ...

public void foo()
{
    System.Threading.Timer timer = null; 
    timer = new System.Threading.Timer((obj) =>
                    {
                        bar();
                        timer.Dispose();
                    }, 
                null, 1000, System.Threading.Timeout.Infinite);
}

public void bar()
{
    // do stuff
}

(gracias a Fred Deschenes por la idea de deshacerse del temporizador dentro de la devolución de llamada)

dodgy_coder
fuente
3
Siento que esta es la mejor respuesta en general para retrasar una llamada de función. Sin hilo, sin trabajo en segundo plano, sin dormir. Los temporizadores son muy eficientes y en cuanto a memoria / cpu.
Zyo
1
@Zyo, gracias por tu comentario; sí, los temporizadores son eficientes, y este tipo de demora es útil en muchas situaciones, especialmente cuando interactúas con algo que está fuera de tu control, que no tiene soporte para eventos de notificación.
dodgy_coder
¿Cuándo se deshace del temporizador?
Didier A.
1
Reviviendo un hilo antiguo aquí, pero el temporizador podría disponerse así: public static void CallWithDelay (método de acción, int delay) {Timer timer = null; var cb = new TimerCallback ((estado) => {método (); timer.Dispose ();}); timer = nuevo Timer (cb, null, delay, Timeout.Infinite); } EDITAR: Bueno, parece que no podemos publicar código en los comentarios ... VisualStudio debería formatearlo correctamente cuando lo copie / pegue de todos modos: P
Fred Deschenes
6
@dodgy_coder Incorrecto. El uso de la timervariable local desde dentro de la lambda que se vincula al objeto delegado cbhace que se coloque en una tienda anónima (detalle de implementación de cierre) que hará que el Timerobjeto sea accesible desde la perspectiva del GC mientras el TimerCallbackdelegado en sí sea accesible . En otras palabras, Timerse garantiza que el objeto no se recolectará como basura hasta después de que el grupo de subprocesos invoque el objeto delegado.
cdhowie
15

Aparte de estar de acuerdo con las observaciones de diseño de los comentaristas anteriores, ninguna de las soluciones fue lo suficientemente limpia para mí. .Net 4 proporciona Dispatchery Taskclases que hacen que retrasar la ejecución en el hilo actual sea bastante simple:

static class AsyncUtils
{
    static public void DelayCall(int msec, Action fn)
    {
        // Grab the dispatcher from the current executing thread
        Dispatcher d = Dispatcher.CurrentDispatcher;

        // Tasks execute in a thread pool thread
        new Task (() => {
            System.Threading.Thread.Sleep (msec);   // delay

            // use the dispatcher to asynchronously invoke the action 
            // back on the original thread
            d.BeginInvoke (fn);                     
        }).Start ();
    }
}

Para el contexto, estoy usando esto para eliminar el rebote y estar ICommandvinculado al botón izquierdo del mouse hacia arriba en un elemento de la interfaz de usuario. Los usuarios estaban haciendo doble clic, lo que estaba causando todo tipo de estragos. (Sé que también podría usar Click/ DoubleClickhandlers, pero quería una solución que funcionara con ICommands en todos los ámbitos).

public void Execute(object parameter)
{
    if (!IsDebouncing) {
        IsDebouncing = true;
        AsyncUtils.DelayCall (DebouncePeriodMsec, () => {
            IsDebouncing = false;
        });

        _execute ();
    }
}
cod3monk3y
fuente
7

Parece que el control de la creación de ambos objetos y su interdependencia debe controlarse externamente, en lugar de entre las clases mismas.

Adam Ralph
fuente
+1, esto parece que necesita un orquestador de algún tipo y tal vez una fábrica
ng5000
5

De hecho, es un diseño muy malo, y mucho menos singleton en sí mismo es un mal diseño.

Sin embargo, si realmente necesita retrasar la ejecución, esto es lo que puede hacer:

BackgroundWorker barInvoker = new BackgroundWorker();
barInvoker.DoWork += delegate
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        bar();
    };
barInvoker.RunWorkerAsync();

Sin embargo, esto se invocará bar()en un hilo separado. Si necesita llamar bar()al hilo original, es posible que deba mover la bar()invocación al RunWorkerCompletedcontrolador o hacer un poco de pirateo con SynchronizationContext.

Anton Gogolev
fuente
3

Bueno, tendría que estar de acuerdo con el punto de "diseño" ... pero probablemente puedas usar un monitor para que uno sepa cuando el otro ha pasado la sección crítica ...

    public void foo() {
        // Do stuff!

        object syncLock = new object();
        lock (syncLock) {
            // Delayed call to bar() after x number of ms
            ThreadPool.QueueUserWorkItem(delegate {
                lock(syncLock) {
                    bar();
                }
            });

            // Do more Stuff
        } 
        // lock now released, bar can begin            
    }
Marc Gravell
fuente
2
public static class DelayedDelegate
{

    static Timer runDelegates;
    static Dictionary<MethodInvoker, DateTime> delayedDelegates = new Dictionary<MethodInvoker, DateTime>();

    static DelayedDelegate()
    {

        runDelegates = new Timer();
        runDelegates.Interval = 250;
        runDelegates.Tick += RunDelegates;
        runDelegates.Enabled = true;

    }

    public static void Add(MethodInvoker method, int delay)
    {

        delayedDelegates.Add(method, DateTime.Now + TimeSpan.FromSeconds(delay));

    }

    static void RunDelegates(object sender, EventArgs e)
    {

        List<MethodInvoker> removeDelegates = new List<MethodInvoker>();

        foreach (MethodInvoker method in delayedDelegates.Keys)
        {

            if (DateTime.Now >= delayedDelegates[method])
            {
                method();
                removeDelegates.Add(method);
            }

        }

        foreach (MethodInvoker method in removeDelegates)
        {

            delayedDelegates.Remove(method);

        }


    }

}

Uso:

DelayedDelegate.Add(MyMethod,5);

void MyMethod()
{
     MessageBox.Show("5 Seconds Later!");
}
David O'Donoghue
fuente
1
Aconsejaría poner algo de lógica para evitar que el temporizador se ejecute cada 250 milisegundos. Primero: puede aumentar el retraso a 500 milisegundos porque su intervalo mínimo permitido es de 1 segundo. Segundo: puede iniciar el temporizador solo cuando se agreguen nuevos delegados y detenerlo cuando no haya más delegados. No hay razón para seguir usando ciclos de CPU cuando no hay nada que hacer. Tercero: puede establecer el intervalo del temporizador en el retraso mínimo para todos los delegados. Por lo tanto, se despierta solo cuando necesita invocar a un delegado, en lugar de despertarse cada 250 milisegundos para ver si hay algo que hacer.
Pic Mickael
MethodInvoker es un objeto Windows.Forms. ¿Existe alguna alternativa para los desarrolladores web? es decir, algo que no entre en conflicto con System.Web.UI.WebControls.
Fandango68
1

Pensé que la solución perfecta sería tener un temporizador que manejara la acción retrasada. A FxCop no le gusta cuando tiene un intervalo de menos de un segundo. Necesito retrasar mis acciones hasta DESPUÉS de que mi DataGrid haya completado la clasificación por columna. Pensé que un temporizador de un disparo (AutoReset = false) sería la solución, y funciona perfectamente. ¡Y FxCop no me permitirá suprimir la advertencia!

Jim Mahaffey
fuente
1

Esto funcionará en versiones anteriores de .NET
Contras: se ejecutará en su propio hilo

class CancelableDelay
    {
        Thread delayTh;
        Action action;
        int ms;

        public static CancelableDelay StartAfter(int milliseconds, Action action)
        {
            CancelableDelay result = new CancelableDelay() { ms = milliseconds };
            result.action = action;
            result.delayTh = new Thread(result.Delay);
            result.delayTh.Start();
            return result;
        }

        private CancelableDelay() { }

        void Delay()
        {
            try
            {
                Thread.Sleep(ms);
                action.Invoke();
            }
            catch (ThreadAbortException)
            { }
        }

        public void Cancel() => delayTh.Abort();

    }

Uso:

var job = CancelableDelay.StartAfter(1000, () => { WorkAfter1sec(); });  
job.Cancel(); //to cancel the delayed job
altair
fuente
0

No existe una forma estándar de retrasar una llamada a una función que no sea usar un temporizador y eventos.

Esto suena como el patrón anti GUI de retrasar una llamada a un método para que pueda estar seguro de que el formulario se ha terminado de diseñar. No es Buena idea.

ng5000
fuente
0

Sobre la base de la respuesta de David O'Donoghue, aquí hay una versión optimizada del Delegado retrasado:

using System.Windows.Forms;
using System.Collections.Generic;
using System;

namespace MyTool
{
    public class DelayedDelegate
    {
       static private DelayedDelegate _instance = null;

        private Timer _runDelegates = null;

        private Dictionary<MethodInvoker, DateTime> _delayedDelegates = new Dictionary<MethodInvoker, DateTime>();

        public DelayedDelegate()
        {
        }

        static private DelayedDelegate Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new DelayedDelegate();
                }

                return _instance;
            }
        }

        public static void Add(MethodInvoker pMethod, int pDelay)
        {
            Instance.AddNewDelegate(pMethod, pDelay * 1000);
        }

        public static void AddMilliseconds(MethodInvoker pMethod, int pDelay)
        {
            Instance.AddNewDelegate(pMethod, pDelay);
        }

        private void AddNewDelegate(MethodInvoker pMethod, int pDelay)
        {
            if (_runDelegates == null)
            {
                _runDelegates = new Timer();
                _runDelegates.Tick += RunDelegates;
            }
            else
            {
                _runDelegates.Stop();
            }

            _delayedDelegates.Add(pMethod, DateTime.Now + TimeSpan.FromMilliseconds(pDelay));

            StartTimer();
        }

        private void StartTimer()
        {
            if (_delayedDelegates.Count > 0)
            {
                int delay = FindSoonestDelay();
                if (delay == 0)
                {
                    RunDelegates();
                }
                else
                {
                    _runDelegates.Interval = delay;
                    _runDelegates.Start();
                }
            }
        }

        private int FindSoonestDelay()
        {
            int soonest = int.MaxValue;
            TimeSpan remaining;

            foreach (MethodInvoker invoker in _delayedDelegates.Keys)
            {
                remaining = _delayedDelegates[invoker] - DateTime.Now;
                soonest = Math.Max(0, Math.Min(soonest, (int)remaining.TotalMilliseconds));
            }

            return soonest;
        }

        private void RunDelegates(object pSender = null, EventArgs pE = null)
        {
            try
            {
                _runDelegates.Stop();

                List<MethodInvoker> removeDelegates = new List<MethodInvoker>();

                foreach (MethodInvoker method in _delayedDelegates.Keys)
                {
                    if (DateTime.Now >= _delayedDelegates[method])
                    {
                        method();

                        removeDelegates.Add(method);
                    }
                }

                foreach (MethodInvoker method in removeDelegates)
                {
                    _delayedDelegates.Remove(method);
                }
            }
            catch (Exception ex)
            {
            }
            finally
            {
                StartTimer();
            }
        }
    }
}

La clase podría mejorarse un poco más utilizando una clave única para los delegados. Porque si agrega el mismo delegado por segunda vez antes de que se active el primero, es posible que tenga un problema con el diccionario.

Pic Mickael
fuente
0
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));
}
Koray
fuente