การเรียกฟังก์ชันล่าช้า


92

มีวิธีง่ายๆที่ดีในการหน่วงเวลาการเรียกใช้ฟังก์ชันในขณะที่ปล่อยให้เธรดดำเนินการต่อไปหรือไม่?

เช่น

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
}

ฉันทราบว่าสามารถทำได้โดยใช้ตัวจับเวลาและตัวจัดการเหตุการณ์ แต่ฉันสงสัยว่ามีวิธี c # มาตรฐานในการบรรลุสิ่งนี้หรือไม่?

ถ้าใครอยากรู้อยากเห็นเหตุผลที่ต้องมีก็คือ foo () และ bar () อยู่ในคลาส (singleton) ที่แตกต่างกันซึ่งฉันต้องการเรียกกันและกันในสถานการณ์พิเศษ ปัญหาที่เกิดขึ้นเมื่อ initialisation ดังนั้น foo จึงต้องเรียก bar ซึ่งต้องการอินสแตนซ์ของคลาส foo ที่กำลังสร้างขึ้น ... ด้วยเหตุนี้การโทรไปยัง bar () ที่ล่าช้าเพื่อให้แน่ใจว่า foo ได้รับการอินสแตนซ์อย่างสมบูรณ์ ... อ่านด้านหลังนี้ เกือบตีดีไซน์ไม่ดี!

แก้ไข

ฉันจะนำประเด็นเกี่ยวกับการออกแบบที่ไม่ดีภายใต้คำแนะนำ! ฉันคิดมานานแล้วว่าฉันจะสามารถปรับปรุงระบบได้อย่างไรก็ตามสถานการณ์ที่น่ารังเกียจนี้จะเกิดขึ้นก็ต่อเมื่อมีข้อยกเว้นเกิดขึ้นในบางครั้งเสื้อกล้ามทั้งสองก็อยู่ร่วมกันได้อย่างดีเยี่ยม ฉันคิดว่าฉันจะไม่ยุ่งกับ async-patters ที่น่ารังเกียจ แต่ฉันจะปรับเปลี่ยนการเริ่มต้นของคลาสใดคลาสหนึ่ง


คุณต้องแก้ไข แต่ไม่ใช่โดยใช้เธรด (หรือการปฏิบัติ asyn อื่น ๆ สำหรับเรื่องนั้น)
ShuggyCoUk

1
การต้องใช้เธรดเพื่อซิงโครไนซ์การเริ่มต้นอ็อบเจ็กต์เป็นสัญญาณว่าคุณควรใช้วิธีอื่น Orchestrator ดูเหมือนจะเป็นตัวเลือกที่ดีกว่า
thinkbeforecoding

1
คืนชีพ! - แสดงความคิดเห็นเกี่ยวกับการออกแบบคุณสามารถเลือกได้ว่าจะเริ่มต้นสองขั้นตอน วาดจาก Unity3D API มีAwakeและStartเฟส ในAwakeเฟสคุณกำหนดค่าตัวเองและในตอนท้ายของเฟสนี้วัตถุทั้งหมดจะเริ่มต้น ในช่วงStartระยะนี้วัตถุสามารถเริ่มสื่อสารกันได้
cod3monk3y

1
คำตอบที่ยอมรับจะต้องมีการเปลี่ยนแปลง
Brian Webster

คำตอบ:


178

ขอบคุณ C # 5/6 ที่ทันสมัย ​​:)

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

public void bar()
{
    // do stuff
}

15
คำตอบนี้ยอดเยี่ยมด้วยเหตุผล 2 ประการ ความเรียบง่ายของโค้ดและความจริงที่ว่า Delay ไม่ได้สร้างเธรดหรือใช้เธรดพูลเหมือน Task.Run หรือ Task.StartNew อื่น ๆ ... เป็นการจับเวลาภายใน
Zyo

ทางออกที่ดี
x4h1d

6
นอกจากนี้โปรดสังเกตเวอร์ชันที่เทียบเท่ากันเล็กน้อย (IMO): Task.Delay (TimeSpan.FromSeconds (1)) ContinueWith (_ => bar ());
Taran

5
@ Zyo จริงๆแล้วมันใช้เธรดอื่น ลองเข้าถึงองค์ประกอบ UI จากมันและจะทำให้เกิดข้อยกเว้น
TudorT

@TudorT - ถ้า Zyo ถูกต้องว่ารันบนเธรดที่มีอยู่แล้วซึ่งรันเหตุการณ์ตัวจับเวลาประเด็นของเขาก็คือมันไม่ใช้ทรัพยากรเพิ่มเติมในการสร้างเธรดใหม่หรือเข้าคิว ไปยังเธรดพูล (แม้ว่าฉันไม่รู้ว่าการสร้างตัวจับเวลานั้นถูกกว่าการจัดคิวงานไปยังเธรดพูลอย่างมากหรือไม่ซึ่งยังไม่ได้สร้างเธรดซึ่งเป็นจุดรวมของเธรดพูล)
ToolmakerSteve

96

ฉันมองหาสิ่งนี้ด้วยตัวเอง - ฉันได้สิ่งต่อไปนี้แม้ว่าจะใช้ตัวจับเวลา แต่ก็ใช้เพียงครั้งเดียวสำหรับการหน่วงเวลาเริ่มต้นและไม่ต้องใช้Sleepสายใด ๆ...

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
}

(ขอบคุณFred Deschenesสำหรับแนวคิดในการทิ้งตัวจับเวลาภายในการโทรกลับ)


3
ฉันรู้สึกว่านี่เป็นคำตอบที่ดีที่สุดโดยทั่วไปในการชะลอการเรียกฟังก์ชัน ไม่มีเธรดไม่มีการทำงานพื้นหลังไม่ต้องนอนหลับ ตัวจับเวลามีประสิทธิภาพมากและหน่วยความจำ / ซีพียูฉลาด
Zyo

1
@Zyo ขอบคุณสำหรับความคิดเห็นของคุณ - ตัวจับเวลามีประสิทธิภาพและความล่าช้าแบบนี้มีประโยชน์ในหลาย ๆ สถานการณ์โดยเฉพาะอย่างยิ่งเมื่อเชื่อมต่อกับบางสิ่งที่อยู่นอกเหนือการควบคุมของคุณซึ่งไม่มีการรองรับเหตุการณ์การแจ้งเตือนใด ๆ
dodgy_coder

คุณทิ้งเครื่องตั้งเวลาเมื่อใด
Didier A.

1
การฟื้นฟูเธรดเก่าที่นี่ แต่ตัวจับเวลาสามารถกำจัดได้เช่นนี้ CallWithDelay โมฆะแบบคงที่สาธารณะ (วิธีการดำเนินการ, การหน่วงเวลา int) {ตัวจับเวลาตัวจับเวลา = null; var cb = TimerCallback ใหม่ ((state) => {method (); timer.Dispose ();}); ตัวจับเวลา = ตัวจับเวลาใหม่ (cb, null, delay, Timeout.Infinite); } แก้ไข: ดูเหมือนว่าเราไม่สามารถโพสต์โค้ดในความคิดเห็นได้ ... VisualStudio ควรจัดรูปแบบให้ถูกต้องเมื่อคุณคัดลอก / วางต่อไป: P
Fred Deschenes

6
@dodgy_coder ผิด. การใช้timerตัวแปรโลคัลจากภายในแลมบ์ดาที่ถูกผูกไว้กับอ็อบเจ็กต์ผู้รับมอบสิทธิ์cbทำให้ถูกยกไปยังที่เก็บ anon (รายละเอียดการใช้งานการปิด) ซึ่งจะทำให้Timerอ็อบเจ็กต์สามารถเข้าถึงได้จากมุมมองของ GC ตราบเท่าที่TimerCallbackผู้ได้รับมอบหมายนั้นสามารถเข้าถึงได้ . กล่าวอีกนัยหนึ่งTimerอ็อบเจ็กต์ได้รับการรับรองว่าจะไม่ถูกเก็บรวบรวมจนกว่าอ็อบเจ็กต์ผู้รับมอบสิทธิ์จะถูกเรียกใช้โดยเธรดพูล
cdhowie

15

นอกเหนือจากการเห็นด้วยกับการสังเกตการออกแบบของผู้แสดงความคิดเห็นก่อนหน้านี้แล้วไม่มีวิธีแก้ปัญหาใดที่สะอาดเพียงพอสำหรับฉัน .Net 4 จัดเตรียมDispatcherและTaskคลาสที่ทำให้การดำเนินการล่าช้าในเธรดปัจจุบันค่อนข้างง่าย:

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

สำหรับบริบทฉันใช้สิ่งนี้เพื่อลบการICommandเชื่อมโยงกับปุ่มซ้ายของเมาส์ที่อยู่บนองค์ประกอบ UI ผู้ใช้กำลังดับเบิลคลิกซึ่งก่อให้เกิดความเสียหายทุกรูปแบบ (ฉันรู้ว่าฉันสามารถใช้Click/ DoubleClickตัวจัดการได้ แต่ฉันต้องการโซลูชันที่ใช้ได้กับICommands ทั่วทั้งบอร์ด)

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

        _execute ();
    }
}

7

ดูเหมือนว่าการควบคุมการสร้างวัตถุทั้งสองนี้และการพึ่งพาซึ่งกันและกันจำเป็นต้องควบคุมจากภายนอกมากกว่าระหว่างชั้นเรียนด้วยกันเอง


+1 ดูเหมือนว่าคุณต้องมีออเคสตเตอร์บางประเภทและอาจจะเป็นโรงงาน
ng5000

5

มันเป็นการออกแบบที่แย่มากนับประสาอะไรกับการออกแบบที่ไม่ดี

อย่างไรก็ตามหากคุณจำเป็นต้องชะลอการดำเนินการจริงๆนี่คือสิ่งที่คุณสามารถทำได้:

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

อย่างไรก็ตามสิ่งนี้จะเรียกใช้bar()ในเธรดแยกต่างหาก หากคุณต้องการที่จะเรียกbar()ในหัวข้อเดิมที่คุณอาจจำเป็นต้องย้ายbar()ภาวนาที่จะจัดการหรือทำบิตของการแฮ็คกับRunWorkerCompletedSynchronizationContext


3

ฉันต้องเห็นด้วยกับประเด็น "การออกแบบ" ... แต่คุณอาจใช้ Monitor เพื่อแจ้งให้ทราบเมื่ออีกส่วนหนึ่งผ่านส่วนสำคัญ ...

    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            
    }

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

        }


    }

}

การใช้งาน:

DelayedDelegate.Add(MyMethod,5);

void MyMethod()
{
     MessageBox.Show("5 Seconds Later!");
}

1
ฉันขอแนะนำให้ใส่ตรรกะบางอย่างเพื่อหลีกเลี่ยงตัวจับเวลาให้ทำงานทุก ๆ 250 มิลลิวินาที ขั้นแรก: คุณสามารถเพิ่มการหน่วงเวลาเป็น 500 มิลลิวินาทีได้เนื่องจากช่วงเวลาต่ำสุดที่อนุญาตคือ 1 วินาที ประการที่สอง: คุณสามารถเริ่มตัวจับเวลาได้เฉพาะเมื่อมีการเพิ่มผู้รับมอบสิทธิ์ใหม่และหยุดการจับเวลาเมื่อไม่มีผู้รับมอบสิทธิ์อีกต่อไป ไม่มีเหตุผลที่จะใช้รอบ CPU ต่อไปเมื่อไม่มีอะไรต้องทำ ประการที่สาม: คุณสามารถตั้งค่าช่วงเวลาเป็นความล่าช้าต่ำสุดสำหรับผู้รับมอบสิทธิ์ทั้งหมด ดังนั้นจะตื่นขึ้นมาก็ต่อเมื่อจำเป็นต้องเรียกผู้ร่วมประชุมแทนที่จะตื่นขึ้นมาทุกๆ 250 มิลลิวินาทีเพื่อดูว่ามีอะไรต้องทำหรือไม่
Pic Mickael

MethodInvoker เป็นวัตถุ Windows.Forms มีทางเลือกอื่นสำหรับนักพัฒนาเว็บหรือไม่? กล่าวคือสิ่งที่ไม่ขัดแย้งกับ System.Web.UI.WebControls
Fandango68

1

แม้ว่าวิธีแก้ปัญหาที่สมบูรณ์แบบคือการมีตัวจับเวลาจัดการกับการกระทำที่ล่าช้า FxCop ไม่ชอบเมื่อคุณมีช่วงเวลาน้อยกว่าหนึ่งวินาที ฉันต้องชะลอการดำเนินการของฉันจนกว่าหลังจากที่ DataGrid ของฉันจัดเรียงตามคอลัมน์เสร็จสิ้น ฉันคิดว่าตัวจับเวลาแบบ one-shot (AutoReset = false) น่าจะเป็นวิธีแก้ปัญหาและทำงานได้อย่างสมบูรณ์แบบ และ FxCop จะไม่ให้ฉันระงับคำเตือน!


1

สิ่งนี้จะใช้ได้กับ. NET
Cons รุ่นเก่ากว่า: จะดำเนินการในเธรดของตัวเอง

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

    }

การใช้งาน:

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

0

ไม่มีวิธีมาตรฐานในการหน่วงเวลาการโทรไปยังฟังก์ชันอื่นนอกจากใช้ตัวจับเวลาและเหตุการณ์

ฟังดูคล้ายกับรูปแบบการป้องกัน GUI ในการหน่วงเวลาการโทรไปยังเมธอดเพื่อให้คุณมั่นใจได้ว่าฟอร์มเสร็จสิ้นแล้ว ไม่ใช่ความคิดที่ดี


0

จากคำตอบของ David O'Donoghue นี่คือเวอร์ชันที่ได้รับการปรับให้เหมาะสมที่สุดของ 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();
            }
        }
    }
}

ชั้นเรียนสามารถปรับปรุงได้อีกเล็กน้อยโดยใช้คีย์เฉพาะสำหรับผู้ร่วมประชุม เนื่องจากถ้าคุณเพิ่มผู้รับมอบสิทธิ์คนเดิมเป็นครั้งที่สองก่อนที่คนแรกจะเริ่มทำงานคุณอาจมีปัญหากับพจนานุกรม


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));
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.