什么是“权利”?在事件发生后将c#中的方法延迟一段时间,同时仍让原始线程运行

本文关键字:一段时间 延迟 方法 线程 运行 原始 权利 事件 什么 | 更新日期: 2023-09-27 18:15:09

我有一个c#项目(使用WPF),涉及用户在画布上放置2d标记以生成3d几何图形。当2d布局发生变化时,会发生大量的数学运算,然后创建一个新的3d视图。执行此计算需要很短但明显的时间。

使用事件和属性,我设置了程序,使其可以自我更新。我有一个类,代表一个项目作为一个整体,它有一个属性,我命名为"Is3dViewValid"。当项目中存储的任何对象的属性发生变化时,都可以将此属性设置为false,从而触发重新生成3d数据的事件。

然而,由于再生需要大量的时间,当用户执行一个不断更新子对象(特别是在画布上拖动一个标记)的操作时,它会导致UI延迟。延迟不足以让我想要将再生移动到一个单独的线程,并处理试图用我有限的技能使所有内容线程安全的艰巨任务……但这段时间太长了,不能就这么算了。

所以,我想做的是等待,直到有限的时间(说1秒)已经过去了,因为最后一次Is3dViewValid被设置为假之前,实际提交到计算。如果用户在短时间内连续进行了几次更改,我希望它在最后一次更改后15秒才重新生成3d数据。

在c#中是否有"正确"的方法来做到这一点?我假设,也许是错误的,完成这将需要第二个线程等待并观察共享的DateTime对象,然后尝试在原始线程上调用一个方法,当一定的时间已经过去了,因为上次Is3dViewValid被设置为false。这是对的吗,还是有更好的方法?

编辑:我在写最初的问题时没有想到这一点,但我想要完成的是,如果事件在t=0.1s, t=0.2s和t=0.3s时被触发,我希望再生方法在t=1.3s时运行一次

什么是“权利”?在事件发生后将c#中的方法延迟一段时间,同时仍让原始线程运行

我首先查看。net的响应式扩展,这是一个处理事件流的框架(通常是示例中的用户交互事件),然后将这些事件作为流来操作。对你最有用的就是http://msdn.microsoft.com/en-us/library/hh229400(v=vs.103).aspx Observable了。此方法允许您指定在将任何更改传播到流的其余部分之前等待的时间,以便您可以通过事件订阅对其采取操作。

进一步资源:

    响应式扩展入门指南
  • 101 RX示例,如果你想直接进入代码
  • 一个伟大的教程从汤到坚果
这里列出的

是一种更简洁的方式来表达与前面所写的相同的东西,但也包括异步执行所需的代码。

public class ApplicationPresenterRX
{
    private DateTime started;
    public Project Project { get; set; }
    public ApplicationPresenterRX()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        // Convert events into observable stream of events based on custom event.
        Observable.FromEvent(ev => { this.Project.Invalidated += () => ev(); },
                         ev => { this.Project.Invalidated -= () => ev();})
            // Only propagate a maximum of 1 event per second (dropping others) 
            .Throttle(TimeSpan.FromSeconds(1))
            // Finally execute the task needed asynchronously
            // The only caveat is that if the code in Project.HeftyComputation updates UI components in WPF you may need to marshal the UI updates onto the correct thread to make it work.
            .Subscribe(e => { Task.Run(Project.HeftyComputation); });
        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }
}

如果你使用的是。net 4.5,你可以在你的事件处理程序中这样做:

Task.Delay(TimeSpan.FromSeconds(5))
    .ContinueWith(t => { /* do whatever it is that you want delayed */ });

Task.Delay将创建一个Task,除了在完成之前等待一定的时间外,它什么都不做。ContinueWith(Action<Task> action)表示"当这个任务完成后,才执行这个action指定的第二个任务。"

如果你还在使用c# 5.0,那么你可以使用async/await工具和async事件处理程序来执行await Task.Delay(TimeSpan.FromSeconds(5));,后面跟着你想要延迟的代码。


我同意Norman H关于研究响应式扩展的建议。它们是一个非常强大的工具集,但它可能比你想要的更多。

好吧,我对响应式扩展做了一些研究,我把代码放在一起,我有两个可能的答案。我不知道其中一种方法是否"正确",但两种方法似乎都有效。RX看起来非常强大,但是使用它会扩展我c#能力的极限。

1。

首先,重申这个问题,假设我有这样一个类:

public class Project
{
    public delegate void InvalidateEventHandler();
    public event InvalidateEventHandler Invalidated;
    private void InvalidateMyself() { if (Invalidated != null) Invalidated(); }
    public void HeftyComputation() { Thread.Sleep(2000); }
    public void SimulateUserDoingStuff()
    {
        Thread.Sleep(100);
        InvalidateMyself();
        Thread.Sleep(100);
        InvalidateMyself();
        Thread.Sleep(100);
        InvalidateMyself();
    }
    public Project() { }
}

它保存数据,它不是线程安全的。它有三种方法。其中一个基于其内部数据执行大量计算,以便在数据更改时进行自我更新。这是通过调用InvalidateMyself()函数的内部属性(未演示)来实现的,触发由父类处理的事件,父类将在某个时候决定告诉对象更新自己。最后,我有一个"模拟用户输入"方法,它在t=0.1s、t=0.2s和t=0.3s时调用InvalidateMyself()。

现在,让这个类更新自己的最简单方法是获取父对象,在本例中是一个应用程序呈现者,监听Invalidate事件,并在它出现时直接触发HeftyComputation()方法。观察下面的类:

public class ApplicationPresenterBasic
{
    private DateTime started;
    public Project Project { get; set; }
    public ApplicationPresenterBasic()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        Project.Invalidated += Project_Invalidated;
        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }
    void Project_Invalidated()
    {
        UpdateProject();
    }
    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

这基本上做到了这一点,除了每次对象"无效"时它都会运行HeftyComputation()方法。下面是输出:

Running HeftyComputation() at 0.1010058s
Running HeftyComputation() at 2.203126s
Running HeftyComputation() at 4.3042462s

好了吗?现在让我们假设我想要的行为是让应用程序呈现者在执行HeftyComputation()之前等待15个时间段,并且没有失效。通过这种方式,在t=1.3s时同时处理所有三个更新。

我用了两种方法,一次使用监听线程和System.Windows.Threading Dispatcher,一次使用Reactive Extensions的IObservable。节流

2。解决方案使用Tasks和System.Windows.Dispatcher

我能想到使用后台任务和调度程序的唯一好处是你不依赖任何第三方库。但是,您被锁定为引用WindowsBase程序集,所以我无法想象这将在Mono上工作,如果这对您很重要的话。

public class ApplicationPresenterWindowsDispatcher
{
    private DateTime started;
    public Project Project { get; set; }
    /* Stuff necessary for this solution */
    private delegate void ComputationDelegate();
    private object Mutex = new object();
    private bool IsValid = true;
    private DateTime LastInvalidated;
    private Task ObservationTask;
    private Dispatcher MainThreadDispatcher;
    private CancellationTokenSource TokenSource;
    private CancellationToken Token;
    public void ObserveAndTriggerComputation(CancellationToken ctoken)
    {
        while (true)
        {
            ctoken.ThrowIfCancellationRequested();
            lock (Mutex)
            {
                if (!IsValid && (DateTime.Now - LastInvalidated).TotalSeconds > 1)
                {
                    IsValid = true;
                    ComputationDelegate compute = new ComputationDelegate(UpdateProject);
                    MainThreadDispatcher.BeginInvoke(compute);
                }
            }
        }
    }

    public ApplicationPresenterWindowsDispatcher()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        Project.Invalidated += Project_Invalidated;
        // Set up observation task
        MainThreadDispatcher = Dispatcher.CurrentDispatcher;
        Mutex = new object();
        TokenSource = new CancellationTokenSource();
        Token = TokenSource.Token;
        ObservationTask = Task.Factory.StartNew(() => ObserveAndTriggerComputation(Token), Token);
        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }
    void Project_Invalidated()
    {
        lock (Mutex)
        {
            IsValid = false;
            LastInvalidated = DateTime.Now;
        }
    }
    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

它是这样工作的:当创建应用程序演示者生成一个在后台运行的任务。该任务监视一个表示Project对象是否仍然有效的bool值和一个表示最后一次无效时间的DateTime。当bool值为false且最后一次无效超过1秒时,Task使用Dispatcher调用主线程上的方法,Dispatcher执行HeftyComputation()。失效事件处理程序现在导致主线程简单地更新bool和DateTime,并等待后台线程决定何时运行更新。

输出:

Running HeftyComputation() at 1.3060747s

这似乎行得通。这可能不是最好的方法,而且我不擅长并发,所以如果有人在这里看到错误或问题,请指出来。

3。使用响应式扩展

的解决方案

这是使用响应式扩展的解决方案。幸运的是,NuGet使得将它添加到项目中变得微不足道,但对于我这种技能水平的人来说,使用它是另一回事。Normon H提到它只需要几行代码,事实证明这是真的,但是让这几行代码正确花费的时间比编写Dispatcher解决方案要长得多。

如果你擅长委托和lambda,响应式扩展看起来很棒。但我不是,所以我很挣扎。

public class ApplicationPresenterRX
{
    private DateTime started;
    public Project Project { get; set; }
    public ApplicationPresenterRX()
    {
        // Create the project and subscribe to the invalidation event
        Project = new Project();
        var invalidations = Observable.FromEvent(ev => { this.Project.Invalidated += () => ev(); },
                                                 ev => { this.Project.Invalidated -= () => ev(); });
        var throttledInvalidations = invalidations.Throttle(TimeSpan.FromSeconds(1));
        throttledInvalidations.Subscribe(e => { UpdateProject(); });
        // Simulate the user doing stuff
        started = DateTime.Now;
        Project.SimulateUserDoingStuff();
    }
    void UpdateProject()
    {
        System.Diagnostics.Debug.WriteLine(string.Format("Running HeftyComputation() at {0}s", (DateTime.Now - started).TotalSeconds));
        Project.HeftyComputation();
    }
}

就是这样,三行。我从项目中创建了一个IObservable源。事件失效时,创建第二个IObservable源来抑制第一个源,然后订阅它,以便在被抑制的源激活时调用我的UpdateProject()方法。弄清楚如何正确地调用Observable。使用我的事件的FromEvent方法是这个过程中最难的部分。

输出:

Running HeftyComputation() at 1.3090749s

通过阅读这个网站:http://www.introtorx.com/uat/content/v1.0.10621.0/13_SchedulingAndThreading.html我的结论是,虽然RX使用线程来计时和调度,但默认情况下它不会调用不同线程上的代码。因此,UpdateProject()方法应该在主线程上运行。