Panggilan fungsi tertunda

92

Apakah ada metode sederhana yang bagus untuk menunda panggilan fungsi sambil membiarkan utas terus berjalan?

misalnya

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
}

Saya sadar bahwa ini dapat dicapai dengan menggunakan timer dan event handler, tapi saya bertanya-tanya apakah ada cara c # standar untuk mencapai ini?

Jika ada yang penasaran, alasan mengapa hal ini diperlukan adalah karena foo () dan bar () berada dalam kelas (tunggal) yang berbeda yang saya perlukan untuk memanggil satu sama lain dalam keadaan luar biasa. Masalahnya adalah hal ini dilakukan saat inisialisasi sehingga foo perlu memanggil bar yang membutuhkan turunan dari kelas foo yang sedang dibuat ... oleh karena itu panggilan tertunda ke bar () untuk memastikan bahwa foo sepenuhnya dijalankan .. Membaca ini kembali hampir mirip dengan desain yang buruk!

EDIT

Saya akan mengambil poin tentang desain yang buruk di bawah saran! Saya sudah lama berpikir bahwa saya mungkin dapat meningkatkan sistem, namun, situasi buruk ini hanya terjadi ketika pengecualian dilemparkan, di lain waktu kedua lajang hidup berdampingan dengan sangat baik. Saya pikir saya tidak akan mengacaukan async-patters yang buruk, melainkan saya akan melakukan refactor inisialisasi salah satu kelas.

TK.
sumber
Anda perlu memperbaikinya tetapi tidak dengan menggunakan utas (atau praktik asyn lainnya dalam hal ini)
ShuggyCoUk
1
Keharusan menggunakan utas untuk menyinkronkan inisialisasi objek adalah tanda bahwa Anda harus mengambil cara lain. Orchestrator tampaknya merupakan pilihan yang lebih baik.
thinkbeforecoding
1
Kebangkitan! - Mengomentari desain, Anda dapat membuat pilihan untuk memiliki inisialisasi dua tahap. Menggambar dari Unity3D API, ada Awakedan Startfase. Pada Awakefase ini, Anda mengkonfigurasi diri Anda sendiri, dan pada akhir fase ini semua objek diinisialisasi. Selama Startfase objek dapat mulai berkomunikasi satu sama lain.
cod3monk3y
1
Jawaban yang diterima perlu diubah
Brian Webster

Jawaban:

178

Berkat C # 5/6 modern :)

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

public void bar()
{
    // do stuff
}
Korayem
sumber
15
Jawaban ini luar biasa karena 2 alasan. Kesederhanaan kode dan fakta bahwa Delay TIDAK membuat utas atau menggunakan kumpulan utas seperti Task.Run atau Task.StartNew lainnya ... ini secara internal adalah pengatur waktu.
Zyo
Solusi yang layak.
x4h1d
6
Perhatikan juga versi setara (IMO) yang sedikit lebih bersih: Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran
5
@ Zyo Sebenarnya itu menggunakan utas yang berbeda. Coba akses elemen UI darinya dan itu akan memicu pengecualian.
TudorT
@TudorT - Jika Zyo benar bahwa ia berjalan pada thread yang sudah ada yang menjalankan peristiwa timer, maka maksudnya adalah ia tidak mengkonsumsi sumber daya tambahan untuk membuat thread baru, atau mengantri ke kumpulan thread. (Meskipun saya tidak tahu apakah membuat pengatur waktu secara signifikan lebih murah daripada mengantri tugas ke kumpulan utas - yang JUGA tidak membuat utas, itu menjadi inti dari kumpulan utas.)
ToolmakerSteve
96

Saya sendiri telah mencari sesuatu seperti ini - saya menemukan yang berikut, meskipun menggunakan pengatur waktu, ia hanya menggunakannya sekali untuk penundaan awal, dan tidak memerlukan Sleeppanggilan apa pun ...

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
}

(terima kasih kepada Fred Deschenes untuk gagasan membuang pengatur waktu dalam panggilan balik)

dodgy_coder
sumber
3
Saya merasa ini adalah jawaban terbaik secara umum untuk menunda panggilan fungsi. Tidak ada utas, tidak ada latar belakang yang berfungsi, tidak ada tidur. Timer sangat efisien dan hemat memori / cpu.
Zyo
1
@ Zyo, terima kasih atas komentar Anda - ya, pengatur waktunya efisien, dan penundaan semacam ini berguna dalam banyak situasi, terutama saat berinteraksi dengan sesuatu yang di luar kendali Anda - yang tidak memiliki dukungan untuk acara notifikasi.
dodgy_coder
Kapan Anda Membuang Timer?
Didier A.
1
Menghidupkan kembali utas lama di sini, tetapi pengatur waktu dapat dibuang seperti ini: public static void CallWithDelay (Metode tindakan, int delay) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = Timer baru (cb, null, delay, Timeout.Infinite); } EDIT: Sepertinya kami tidak dapat memposting kode di komentar ... VisualStudio harus memformatnya dengan benar saat Anda menyalin / menempelkannya: P
Fred Deschenes
6
@dodgyodgy Salah. Menggunakan timervariabel lokal dari dalam lambda yang terikat ke objek delegasi cbmenyebabkannya diangkat ke penyimpanan langsung (detail implementasi penutupan) yang akan menyebabkan Timerobjek dapat dijangkau dari perspektif GC selama TimerCallbackdelegasi itu sendiri dapat dijangkau . Dengan kata lain, Timerobjek dijamin tidak akan dikumpulkan sampah sampai setelah objek delegasi dipanggil oleh kumpulan thread.
cdhowie
15

Selain setuju dengan pengamatan desain dari komentator sebelumnya, tidak ada solusi yang cukup bersih untuk saya. .Net 4 menyediakan Dispatcherdan Taskkelas - kelas yang membuat penundaan eksekusi pada thread saat ini cukup sederhana:

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

Untuk konteksnya, saya menggunakan ini untuk ICommandmelepaskan tombol yang diikat ke kiri mouse pada elemen UI. Pengguna mengklik dua kali yang menyebabkan semua jenis malapetaka. (Saya tahu saya juga bisa menggunakan Click/ DoubleClickpenangan, tapi saya ingin solusi yang bekerja dengan ICommands secara keseluruhan).

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

        _execute ();
    }
}
cod3monk3y
sumber
7

Kedengarannya seperti kontrol pembuatan kedua objek ini dan kesalingtergantungannya perlu dikontrol secara eksternal, bukan antar kelas itu sendiri.

Adam Ralph
sumber
+1, sepertinya Anda membutuhkan orkestra dan mungkin pabrik
ng5000
5

Ini memang desain yang sangat buruk, apalagi singleton dengan sendirinya adalah desain yang buruk.

Namun, jika Anda benar-benar perlu menunda eksekusi, inilah yang dapat Anda lakukan:

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

Namun, ini akan dijalankan bar()pada utas terpisah. Jika Anda perlu memanggil bar()di utas asli, Anda mungkin perlu memindahkan bar()pemanggilan ke RunWorkerCompletedpenangan atau melakukan sedikit peretasan SynchronizationContext.

Anton Gogolev
sumber
3

Yah, saya harus setuju dengan poin "desain" ... tetapi Anda mungkin dapat menggunakan Monitor untuk memberi tahu yang satu ketika yang lain melewati bagian kritis ...

    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
sumber
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);

        }


    }

}

Pemakaian:

DelayedDelegate.Add(MyMethod,5);

void MyMethod()
{
     MessageBox.Show("5 Seconds Later!");
}
David O'Donoghue
sumber
1
Saya akan menyarankan untuk meletakkan beberapa logika untuk menghindari timer berjalan setiap 250 milidetik. Pertama: Anda dapat meningkatkan penundaan menjadi 500 milidetik karena interval minimum yang diizinkan adalah 1 detik. Kedua: Anda dapat memulai timer hanya ketika delegasi baru ditambahkan, dan menghentikannya ketika tidak ada lagi delegasi. Tidak ada alasan untuk tetap menggunakan siklus CPU saat tidak ada yang bisa dilakukan. Ketiga: Anda dapat mengatur interval pengatur waktu ke penundaan minimum di semua delegasi. Jadi, ia hanya bangun jika perlu memanggil delegasi, alih-alih bangun setiap 250 milidetik untuk melihat apakah ada yang harus dilakukan.
Pic Mickael
MethodInvoker adalah objek Windows.Forms. Apakah ada alternatif untuk pengembang Web? yaitu: sesuatu yang tidak berbenturan dengan System.Web.UI.WebControls.
Fandango68
1

Saya pikir solusi yang tepat adalah memiliki pengatur waktu menangani tindakan yang tertunda. FxCop tidak suka jika Anda memiliki interval kurang dari satu detik. Saya perlu menunda tindakan saya sampai SETELAH DataGrid saya selesai mengurutkan menurut kolom. Saya pikir timer sekali pakai (AutoReset = false) akan menjadi solusinya, dan berfungsi dengan sempurna. DAN, FxCop tidak akan membiarkan saya menekan peringatan tersebut!

Jim Mahaffey
sumber
1

Ini akan berfungsi baik pada versi .NET
Kontra: akan dijalankan di utasnya sendiri

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

    }

Pemakaian:

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

Tidak ada cara standar untuk menunda panggilan ke suatu fungsi selain menggunakan timer dan acara.

Ini terdengar seperti pola anti GUI untuk menunda panggilan ke suatu metode sehingga Anda bisa yakin formulir telah selesai disusun. Bukan ide yang bagus.

ng5000
sumber
0

Berdasarkan jawaban dari David O'Donoghue, berikut adalah versi Delayed Delegate yang dioptimalkan:

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

Kelas bisa sedikit lebih ditingkatkan dengan menggunakan kunci unik untuk para delegasi. Karena jika Anda menambahkan delegasi yang sama untuk kedua kalinya sebelum yang pertama diaktifkan, Anda mungkin mendapatkan masalah dengan kamus.

Foto Mickael
sumber
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
sumber