【问题标题】:Run code only once using dispatcher使用调度程序只运行一次代码
【发布时间】:2014-10-15 14:43:45
【问题描述】:

我有一个只运行一次代码的简单模式。它主要用于更新 UI 上的某些内容,而它可能会在后台经常更改。

private bool _updating;
private void UpdateSomething()
{
    if (!_updating)
    {
        _updating = true;

        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                _updating = false;
                DoSomething();
            }), DispatcherPriority.Background);
    }
}

我更愿意将样板代码放在一个简单的方法中:

public static void RunOnce(Action action, ref bool guard)
{
    if (!guard)
    {
        guard = true;

        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                guard = false;
                action();
            }), DispatcherPriority.Background);
    }
}

这样称呼它:

void UpdateSomething()
{
    RunOnce(DoSomething, ref _updating);
}

但是,这不起作用,因为您不能在匿名方法中使用 ref 参数。 是否有任何解决方法,例如在方法执行时固定 ref 参数并释放它?

【问题讨论】:

  • 你不能像这样private static bool guard = false 将 bool 保护声明为私有静态吗?还请查看此帖子,例如如何在匿名方法 stackoverflow.com/questions/23630765/… 中声明 ref 参数
  • 将保护设为静态不是一种选择,因为我会单独更新多个实例。但是,将其放入链接线程中的引用类型将是一种选择。

标签: c# dispatcher ref


【解决方案1】:

你可以这样做:

public static void RunOnce(Action action, ref RunOnceToken token)
{
    if (token == null || token.IsCompleted)
    {
        token = new RunOnceToken(
            Application.Current.Dispatcher.BeginInvoke(
                action,
                DispatcherPriority.Background));
    }
}

public sealed class RunOnceToken : IDisposable
{
    private DispatcherOperation _operation;

    public RunOnceToken(DispatcherOperation operation)
    {
        if (operation != null &&
            operation.Status != DispatcherOperationStatus.Completed &&
            operation.Status != DispatcherOperationStatus.Aborted)
        {
            _operation = operation;
            _operation.Completed += OnCompletedOrAborted;
            _operation.Aborted += OnCompletedOrAborted;
        }
    }

    private void OnCompletedOrAborted(object sender, EventArgs e)
    {
        this.Dispose();
    }

    public bool IsCompleted
    {
        get { return _operation == null; }
    }

    public void Dispose()
    {
        var operation = _operation;
        if (operation == null)
            return;

        _operation = null;

        operation.Completed -= OnCompletedOrAborted;
        operation.Aborted -= OnCompletedOrAborted;
    }
}

您的示例用法将更改为:

private RunOnceToken _updateToken;

private void UpdateSomething()
{
    RunOnce(DoSomething, ref _updateToken);
}

如果您从不清除令牌副本并不重要,因为包裹的DispatcherOperation 在完成时会被清除,以避免泄漏action 或其捕获的任何值。

如果不是很明显,这些都不是并发安全的;我假设以上所有内容都只能从 UI 线程访问。

一个有用的改进可能是向RunOnce 添加一个可选的DispatcherPriority 参数,以便您可以控制用于调度action 的优先级(如果它被安排在较低的优先级,则可能取消已调度的操作)。

【讨论】:

  • 这是一个有趣的方法。实际上,这是在 UI Thread 中使用的,因为我只是想更新一个集合(或多个对象)中的许多项目,并且当此操作完成时(UI-Thread 再次空闲),更改将呈现在 UI 上,例如使用 Linq。只是出于好奇:如果从另一个线程调用它会发生什么最糟糕的情况?我认为,Update 方法可以运行多次,但在一个线程上。因此,只要处理后的数据是线程安全的,就不会发生任何不好的事情。
  • 嗯,根据您的更新代码实际执行的操作,可能存在危险:如果您在更新已经在进行时调用RunOnce,您不知道该更新有多远.那么,更新可能不会反映您的最新数据,并且由于尚未完成,因此您不会安排另一个更新。您可以将其重新设计为更具侵略性,并在操作开始而不是完成(或中止)时清除其“计划”状态。最好有额外的更新而不是错过一个。有一些不同步的读取和写入正在进行,但它们看起来大多是良性的。
  • 哦,是的,我没注意到。事实上,这就是我在调度程序操作开始时重置守卫的原因。无论如何,从另一个线程调用该方法会破坏其目的,因为在这种情况下,UI 线程空闲并且会过早处理更新的可能性很大......
  • 嘿,我已经根据你的答案发布了一个答案,但我不确定它是否正确,你能检查一下吗?
【解决方案2】:

我不知道DispatcherOperation 的存在,但是看到 Mike Strobel 的回答我写了以下代码。我不是 100% 确定它,但它似乎可以在没有太多样板的情况下工作。

public static class DispatcherExtensions {
    public static int clearInterval = 10_000;

    private static long time => DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
    private static long lastClearTime = time;
    private static Dictionary<int, DispatcherOperation> currOperations = new Dictionary<int, DispatcherOperation>();
    private static object sync = new object();

    public static void invokeLastAsync(this Dispatcher d,  Action a, DispatcherPriority p = DispatcherPriority.Background, [CallerFilePath]object key1 = null, [CallerLineNumber]object key2 = null) {
        lock (sync) {
            DispatcherOperation dop;
            var k = key1.GetHashCode() ^ key2.GetHashCode();
            if (currOperations.ContainsKey(k)) {
                dop = currOperations[k];
                currOperations.Remove(k);
                dop.Abort();
            }
            dop = d.BeginInvoke(a, p);
            clearOperations(false);
            currOperations.Add(k, dop);
        }
    }

    public static void clearOperations(bool force = true) {
        var ct = time;
        if (!force && ct - lastClearTime < clearInterval) return;
        var nd = new Dictionary<int, DispatcherOperation>();
        foreach (var ao in currOperations) {
            var s = ao.Value.Status;
            if (s == DispatcherOperationStatus.Completed
                || s == DispatcherOperationStatus.Aborted)
            nd.Add(ao.Key, ao.Value);
        }
        currOperations = nd;
        lastClearTime = ct;
    }

}

基本扩展方法以文件路径和行号为键将DispacherOperation实例存储在字典中,如果该键已经有操作,则将其中止并替换为新操作。定期从不再调用的已完成/中止操作中清除字典。

用法很简单:

private int initCount = 0;
private int invokeCount = 0;
private void updateSomething() {
    initCount++;
    view.Dispatcher.invokeLastAsync(() => {
        Console.WriteLine($@"invoked {++invokeCount}/{initCount}");
    });
}

到目前为止,我还没有遇到任何问题。也许其他人可以看到一些弱点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多