Chamadas de função atrasadas

90

Existe um método simples e agradável de atrasar uma chamada de função enquanto permite que o thread continue em execução?

por exemplo

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
}

Estou ciente de que isso pode ser feito usando um cronômetro e manipuladores de eventos, mas gostaria de saber se existe uma maneira c # padrão de fazer isso.

Se alguém estiver curioso, isso é necessário porque foo () e bar () estão em classes diferentes (singleton) que precisam ser chamadas em circunstâncias excepcionais. O problema é que isso é feito na inicialização, então foo precisa chamar bar, que precisa de uma instância da classe foo que está sendo criada ... daí a chamada atrasada para bar () para garantir que foo esteja totalmente instanciado .. Lendo isso de volta quase cheira a um design ruim!

EDITAR

Vou levar em consideração os pontos sobre design ruim! Há muito tempo pensei que poderia melhorar o sistema, entretanto, essa situação desagradável ocorre quando uma exceção é lançada, em todas as outras vezes os dois singletons coexistem muito bem. Acho que não vou mexer com padrões assíncronos desagradáveis, em vez disso, vou refatorar a inicialização de uma das classes.

TK.
fonte
Você precisa consertar, mas não usando tópicos (ou qualquer outra prática asyn)
ShuggyCoUk
1
Ter que usar threads para sincronizar a inicialização do objeto é o sinal de que você deve seguir outro caminho. O orquestrador parece uma opção melhor.
thinkbeforecoding
1
Ressurreição! - Comentando sobre o design, você pode escolher ter uma inicialização de dois estágios. Com base na API Unity3D, existem Awakee Startfases. Na Awakefase, você se configura e, no final desta fase, todos os objetos são inicializados. Durante a Startfase, os objetos podem começar a se comunicar uns com os outros.
cod3monk3y
1
A resposta aceita precisa ser alterada
Brian Webster

Respostas:

171

Graças ao C # 5/6 moderno :)

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

public void bar()
{
    // do stuff
}
Korayem
fonte
14
Essa resposta é incrível por 2 motivos. Simplicidade do código e o fato de que o Delay NÃO cria um thread nem usa o pool de threads como outro Task.Run ou Task.StartNew ... é internamente um cronômetro.
Zyo,
Uma solução decente.
x4h1d
5
Observe também uma versão equivalente ligeiramente mais limpa (IMO): Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran
4
@Zyo Na verdade, ele usa um tópico diferente. Tente acessar um elemento de IU a partir dele e isso irá disparar uma exceção.
TudorT
@TudorT - Se Zyo estiver correto ao dizer que é executado em um thread já existente que executa eventos de temporizador, então seu ponto é que ele não consome recursos extras criando um novo thread, nem enfileirando para o pool de threads. (Embora eu não saiba se criar um cronômetro é significativamente mais barato do que enfileirar uma tarefa para o pool de threads - o que TAMBÉM não cria um thread, sendo esse o ponto principal do pool de threads.)
ToolmakerSteve
96

Eu mesmo tenho procurado algo parecido com isso - eu vim com o seguinte, embora use um cronômetro, ele usa apenas uma vez para o atraso inicial e não requer nenhuma Sleepchamada ...

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
}

(obrigado a Fred Deschenes pela ideia de descartar o cronômetro dentro do callback)

dodgy_coder
fonte
3
Acho que esta é a melhor resposta em geral para atrasar uma chamada de função. Sem discussão, sem fundo funcionando, sem sono. Os temporizadores são muito eficientes e em termos de memória / CPU.
Zyo
1
@Zyo, obrigado pelo seu comentário - sim, os temporizadores são eficientes e esse tipo de atraso é útil em muitas situações, especialmente ao fazer interface com algo que está fora de seu controle - que não tem suporte para eventos de notificação.
dodgy_coder
Quando você descarta o temporizador?
Didier A.
1
Revivendo um thread antigo aqui, mas o cronômetro pode ser descartado assim: public static void CallWithDelay (Método de ação, atraso interno) {Timer timer = null; var cb = new TimerCallback ((estado) => {método (); timer.Dispose ();}); timer = novo Timer (cb, nulo, atraso, Timeout.Infinite); } EDITAR: Bem, parece que não podemos postar código nos comentários ... VisualStudio deve formatá-lo corretamente quando você o copia / cola de qualquer maneira: P
Fred Deschenes
6
@dodgy_coder errado. Usar a timervariável local de dentro do lambda que é vinculado ao objeto delegado cbfaz com que ele seja içado para um armazenamento anon (detalhe de implementação de fechamento) que fará com que o Timerobjeto seja alcançável da perspectiva do GC enquanto o TimerCallbackpróprio delegado estiver acessível . Em outras palavras, Timeré garantido que o objeto não será coletado como lixo até que o objeto delegado seja chamado pelo pool de threads.
cdhowie
15

Além de concordar com as observações de design dos comentadores anteriores, nenhuma das soluções foi suficientemente limpa para mim. .Net 4 fornece Dispatchere Taskclasses que tornam bastante simples atrasar a execução no thread atual :

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

Por contexto, estou usando isso para depurar um ICommandbotão esquerdo do mouse vinculado a um elemento de interface do usuário. Os usuários clicam duas vezes, o que está causando todo tipo de confusão. (Sei que também poderia usar Click/ DoubleClickhandlers, mas queria uma solução que funcionasse com ICommands em todos os níveis).

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

        _execute ();
    }
}
cod3monk3y
fonte
7

Parece que o controle da criação de ambos os objetos e sua interdependência precisam ser controlados externamente, em vez de entre as próprias classes.

Adam Ralph
fonte
+1, isso soa como se você precisasse de algum tipo de orquestrador e talvez de uma fábrica
ng5000
5

É realmente um design muito ruim, quanto mais um singleton por si só é um design ruim.

No entanto, se você realmente precisa atrasar a execução, eis o que você pode fazer:

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

No entanto, isso será invocado bar()em um thread separado. Se você precisar chamar bar()o thread original, pode ser necessário mover a bar()invocação para o RunWorkerCompletedmanipulador ou fazer um pouco de hacking SynchronizationContext.

Anton Gogolev
fonte
3

Bem, eu teria que concordar com o ponto de "design" ... mas você provavelmente pode usar um Monitor para avisar um quando o outro já passou da seção 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
fonte
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
fonte
1
Aconselho colocar alguma lógica para evitar que o cronômetro rode a cada 250 milissegundos. Primeiro: você pode aumentar o atraso para 500 milissegundos porque o intervalo mínimo permitido é de 1 segundo. Segundo: você pode iniciar o cronômetro apenas quando novos delegados forem adicionados e interrompê-lo quando não houver mais delegados. Não há razão para continuar usando os ciclos da CPU quando não há nada para fazer. Terceiro: você pode definir o intervalo do cronômetro para o atraso mínimo em todos os delegados. Portanto, ele acorda apenas quando precisa invocar um delegado, em vez de acordar a cada 250 milissegundos para ver se há algo a fazer.
Pic Mickael
MethodInvoker é um objeto Windows.Forms. Existe uma alternativa para desenvolvedores da Web, por favor? ou seja: algo que não entra em conflito com System.Web.UI.WebControls.
Fandango68 de
1

Achei que a solução perfeita seria ter um cronômetro para lidar com a ação atrasada. O FxCop não gosta quando você tem um intervalo de menos de um segundo. Preciso atrasar minhas ações até DEPOIS que meu DataGrid concluiu a classificação por coluna. Achei que um temporizador de disparo único (AutoReset = false) seria a solução e funciona perfeitamente. E, FxCop não me deixa suprimir o aviso!

Jim Mahaffey
fonte
1

Isso funcionará em versões mais antigas do .NET
Cons: será executado em seu próprio thread

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
fonte
0

Não existe uma maneira padrão de atrasar uma chamada para uma função, a não ser usando um cronômetro e eventos.

Isso soa como o anti-padrão da GUI de atrasar uma chamada para um método para que você possa ter certeza de que o layout do formulário foi concluído. Não é uma boa ideia.

ng5000
fonte
0

Com base na resposta de David O'Donoghue, aqui está uma versão otimizada do Delayed Delegate:

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

A classe poderia ser melhorada um pouco mais usando uma chave exclusiva para os delegados. Porque se você adicionar o mesmo delegado uma segunda vez antes de disparar o primeiro, poderá ter um problema com o dicionário.

Pic Mickael
fonte
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
fonte