Các cuộc gọi chức năng bị trì hoãn


92

Có một phương pháp đơn giản hay ho nào để trì hoãn một cuộc gọi hàm trong khi vẫn cho phép luồng tiếp tục thực thi không?

ví dụ

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
}

Tôi biết rằng có thể đạt được điều này bằng cách sử dụng bộ hẹn giờ và trình xử lý sự kiện, nhưng tôi tự hỏi liệu có cách c # tiêu chuẩn nào để đạt được điều này không?

Nếu ai đó tò mò, lý do yêu cầu điều này là foo () và bar () nằm trong các lớp (singleton) khác nhau mà tôi cần phải gọi nhau trong những trường hợp đặc biệt. Vấn đề là điều này được thực hiện khi khởi tạo vì vậy foo cần gọi thanh cần một thể hiện của lớp foo đang được tạo ... do đó, lệnh gọi đến bar () bị trì hoãn để đảm bảo rằng foo được cài đặt hoàn toàn .. Đọc lại phần này gần như thiết kế xấu!

BIÊN TẬP

Tôi sẽ đưa ra những điểm về thiết kế xấu theo lời khuyên! Từ lâu, tôi đã nghĩ rằng mình có thể cải thiện hệ thống, tuy nhiên, tình huống khó chịu này chỉ xảy ra khi một ngoại lệ được ném ra, ở những thời điểm khác, hai singleleton cùng tồn tại rất độc đáo. Tôi nghĩ rằng tôi sẽ không gặp rắc rối với những người bảo vệ không đồng bộ khó chịu, thay vào đó tôi sẽ cấu trúc lại quá trình khởi tạo của một trong các lớp.


Bạn cần phải sửa chữa nó nhưng không phải bằng cách sử dụng chủ đề (hoặc bất kỳ thực hành asyn khác cho rằng vấn đề)
ShuggyCoUk

1
Việc phải sử dụng các luồng để đồng bộ hóa quá trình khởi tạo đối tượng là dấu hiệu cho thấy bạn nên thực hiện một cách khác. Orchestrator có vẻ là một lựa chọn tốt hơn.
thinkbeforecoding 13/02/09

1
Phục sinh! - Nhận xét về thiết kế, bạn có thể đưa ra lựa chọn khởi tạo hai giai đoạn. Lấy từ API Unity3D, có AwakeStartcác giai đoạn. Trong Awakegiai đoạn này, bạn tự định cấu hình và vào cuối giai đoạn này, tất cả các đối tượng được khởi tạo. Trong Startgiai đoạn này, các đối tượng có thể bắt đầu giao tiếp với nhau.
cod3monk3y

1
Nhu cầu câu trả lời được chấp nhận phải được thay đổi
Brian Webster

Câu trả lời:


177

Cảm ơn C # 5/6 hiện đại :)

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

public void bar()
{
    // do stuff
}

15
Câu trả lời này thật tuyệt vời vì 2 lý do. Sự đơn giản của mã và thực tế là Delay KHÔNG tạo một luồng cũng như không sử dụng nhóm luồng như các Task.Run hoặc Task.StartNew khác ... nó nội bộ là một bộ đếm thời gian.
Zyo

Một giải pháp tốt.
x4h1d

5
Cũng lưu ý rằng phiên bản tương đương (IMO) gọn gàng hơn một chút: Task.Delay (TimeSpan.FromSeconds (1)). ContinueWith (_ => bar ());
Taran

5
@Zyo Thực ra nó sử dụng một chủ đề khác. Hãy thử truy cập một phần tử giao diện người dùng từ nó và nó sẽ kích hoạt một ngoại lệ.
TudorT

@TudorT - Nếu Zyo đúng rằng nó chạy trên một chuỗi đã có sẵn chạy các sự kiện hẹn giờ, thì quan điểm của anh ấy là nó không tiêu tốn thêm tài nguyên khi tạo một chuỗi mới, cũng như không phải xếp hàng vào nhóm chuỗi. (Mặc dù tôi không biết liệu việc tạo bộ đếm thời gian có rẻ hơn đáng kể so với việc xếp hàng một nhiệm vụ vào nhóm luồng - điều mà CŨNG không tạo ra một luồng, đó là toàn bộ điểm của nhóm luồng.)
ToolmakerSteve

96

Tôi đã tự mình tìm kiếm một thứ như thế này - tôi đã nghĩ ra điều sau, mặc dù nó sử dụng bộ hẹn giờ, nó chỉ sử dụng một lần cho độ trễ ban đầu và không yêu cầu bất kỳ Sleepcuộc gọi nào ...

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
}

(cảm ơn Fred Deschenes vì ý tưởng sắp xếp bộ đếm thời gian trong cuộc gọi lại)


3
Tôi cảm thấy đây là câu trả lời tốt nhất nói chung để trì hoãn một cuộc gọi hàm. Không có chủ đề, không có nền hoạt động, không ngủ. Bộ hẹn giờ rất hiệu quả và bộ nhớ / cpu khôn ngoan.
Zyo 27/09/12

1
@Zyo, cảm ơn bạn đã nhận xét - có bộ hẹn giờ rất hiệu quả và kiểu chậm trễ này rất hữu ích trong nhiều trường hợp, đặc biệt là khi giao tiếp với thứ gì đó ngoài tầm kiểm soát của bạn - vốn không hỗ trợ bất kỳ sự kiện thông báo nào.
dodgy_coder

Khi nào bạn vứt bỏ bộ hẹn giờ?
Didier A.

1
Đang hồi sinh một chuỗi cũ ở đây, nhưng bộ đếm thời gian có thể được xử lý như thế này: public static void CallWithDelay (Action method, int delay) {Timer timer = null; var cb = new TimerCallback ((state) => {method (); timer.Dispose ();}); timer = new Timer (cb, null, delay, Timeout.Infinite); } CHỈNH SỬA: Có vẻ như chúng tôi không thể đăng mã trong nhận xét ... VisualStudio nên định dạng nó đúng cách khi bạn sao chép / dán nó bằng mọi cách: P
Fred Deschenes

6
@dodgy_coder Sai. Việc sử dụng timerbiến cục bộ từ bên trong lambda được liên kết với đối tượng ủy nhiệm cbkhiến nó được đưa vào một cửa hàng anon (chi tiết triển khai đóng) sẽ khiến Timerđối tượng có thể truy cập được từ góc nhìn của GC miễn TimerCallbacklà có thể truy cập được chính đối tượng đó . Nói cách khác, Timerđối tượng được đảm bảo không bị thu gom rác cho đến sau khi đối tượng ủy nhiệm được gọi bởi nhóm luồng.
cdhowie

15

Ngoài việc đồng ý với quan sát thiết kế của những người bình luận trước đó, không có giải pháp nào đủ sạch đối với tôi. .Net 4 cung cấp DispatcherTaskcác lớp làm cho việc trì hoãn thực thi trên luồng hiện tại khá đơn giản:

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

Đối với ngữ cảnh, tôi đang sử dụng điều này để loại bỏ sự ICommandràng buộc với nút chuột trái trên một phần tử giao diện người dùng. Người dùng đang nhấp đúp vào điều này đã gây ra tất cả các loại tàn phá. (Tôi biết tôi cũng có thể sử dụng Click/ DoubleClicktrình xử lý, nhưng tôi muốn có một giải pháp hoạt động với ICommands trên toàn diện).

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

        _execute ();
    }
}

7

Nghe có vẻ như việc kiểm soát việc tạo ra cả hai đối tượng này và sự phụ thuộc lẫn nhau của chúng cần được kiểm soát từ bên ngoài, thay vì giữa chính các lớp.


+1, điều này âm thanh như bạn cần một Orchestrator của một số loại và có lẽ một nhà máy
ng5000

5

Nó thực sự là một thiết kế rất tệ, hãy để singleton tự nó là một thiết kế tồi.

Tuy nhiên, nếu bạn thực sự cần trì hoãn việc thực thi, đây là những gì bạn có thể làm:

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

Tuy nhiên, điều này sẽ gọi bar()trên một chuỗi riêng biệt. Nếu bạn cần gọi bar()trong chuỗi ban đầu, bạn có thể cần chuyển bar()lời gọi sang RunWorkerCompletedtrình xử lý hoặc thực hiện một chút hack với SynchronizationContext.


3

Chà, tôi phải đồng ý với quan điểm "thiết kế" ... nhưng bạn có thể sử dụng Màn hình để cho một người biết khi người kia đã qua phần quan trọng ...

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

        }


    }

}

Sử dụng:

DelayedDelegate.Add(MyMethod,5);

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

1
Tôi khuyên bạn nên đặt một số logic để tránh bộ đếm thời gian chạy sau mỗi 250 mili giây. Thứ nhất: Bạn có thể tăng độ trễ lên 500 mili giây vì khoảng thời gian cho phép tối thiểu của bạn là 1 giây. Thứ hai: Bạn chỉ có thể bắt đầu hẹn giờ khi thêm đại biểu mới và dừng khi không còn đại biểu nữa. Không có lý do gì để tiếp tục sử dụng chu kỳ CPU khi không có việc gì làm. Thứ ba: bạn có thể đặt khoảng thời gian hẹn giờ thành độ trễ tối thiểu trên tất cả các đại biểu. Vì vậy, nó chỉ thức dậy khi cần gọi một đại biểu, thay vì thức dậy sau mỗi 250 mili giây để xem có việc gì cần làm hay không.
Pic Mickael

MethodInvoker là một đối tượng Windows.Forms. Xin vui lòng có một giải pháp thay thế cho các nhà phát triển Web? tức là: thứ gì đó không xung đột với System.Web.UI.WebControls.
Fandango68,

1

Tôi mặc dù giải pháp hoàn hảo sẽ là có một bộ đếm thời gian xử lý hành động bị trì hoãn. FxCop không thích khi bạn có khoảng thời gian ít hơn một giây. Tôi cần trì hoãn hành động của mình cho đến khi SAU KHI DataGrid của tôi hoàn thành việc sắp xếp theo cột. Tôi đã tìm ra bộ hẹn giờ một lần chụp (AutoReset = false) sẽ là giải pháp và nó hoạt động hoàn hảo. VÀ, FxCop sẽ không cho phép tôi ngừng cảnh báo!


1

Điều này sẽ hoạt động trên các phiên bản cũ hơn của .NET
Nhược điểm: sẽ thực thi trong chuỗi của riêng nó

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

    }

Sử dụng:

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

0

Không có cách tiêu chuẩn nào để trì hoãn cuộc gọi đến một chức năng ngoài việc sử dụng bộ đếm thời gian và sự kiện.

Điều này có vẻ giống như kiểu chống trì hoãn lệnh gọi đến một phương thức của GUI để bạn có thể chắc chắn rằng biểu mẫu đã hoàn tất việc đặt. Không phải là một ý tưởng tốt.


0

Dựa trên câu trả lời từ David O'Donoghue, đây là phiên bản được tối ưu hóa của 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();
            }
        }
    }
}

Lớp có thể được cải thiện hơn một chút bằng cách sử dụng một khóa duy nhất cho các đại biểu. Bởi vì nếu bạn thêm cùng một đại biểu lần thứ hai trước khi người đầu tiên kích hoạt, bạn có thể gặp sự cố với từ điển.


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));
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.