简体   繁体   English

什么是在事件发生后将在C#中运行的方法延迟一定时间,同时又让原始线程运行的“正确”方法?

[英]What's the “right” way to delay a method running in C# for a set amount of time after an event while still letting the original thread run?

I have a project in C# (using WPF) which involves a user placing 2d markers on a canvas to generate 3d geometry. 我有一个C#项目(使用WPF),该项目涉及用户将2d标记放置在画布上以生成3d几何。 When the 2d arrangement changes there's a fair amount of math that happens, and then a new 3d view is created. 当2d排列更改时,会发生大量的数学运算,然后创建一个新的3d视图。 Performing this computation takes a short but noticeable amount of time. 执行此计算需要很短但很明显的时间。

Using events and properties I've set the program up such that it can update itself. 使用事件和属性,我已经设置了程序,以便可以自我更新。 I have a class that represents a project as a whole, and it has a property I've named "Is3dViewValid". 我有一个代表整个项目的类,它具有一个名为“ Is3dViewValid”的属性。 Any one of several objects stored in the project can set this property to false when their own properties change, triggering an event which regenerates the 3d data. 当存储在项目中的多个对象中的任何一个更改其自身的属性时,可以将其设置为false,从而触发一个事件,该事件重新生成3d数据。

However, since the regeneration takes a noticeable amount of time, it causes the UI to lag when the user is performing an action that is constantly updating one of the child objects (specifically dragging a marker across the canvas). 但是,由于重新生成需要花费大量时间,因此当用户执行不断更新子对象之一(具体是在画布上拖动标记)的操作时,它将导致UI滞后。 The delay isn't long enough for me to want to move the regeneration to a separate thread and deal with the monumental task of trying to make everything thread-safe with my limited skills... but it's too long to just let it be. 延迟还不足以让我想将更新移到一个单独的线程上,并完成一项艰巨的任务,即以我有限的技能来尝试使所有线程安全,但是让它成为现实已经太久了。

So, what I would like to do is wait until a finite amount of time (say 1 second) has passed since the last time Is3dViewValid is set to false before actually committing to the computation. 因此,我想做的是等到最后一次将Is3dViewValid设置为false之后,经过有限的时间(例如1秒),然后才真正进行计算。 If the user makes several changes in short succession I want it to wait until 1s after the last change before it regenerates the 3d data. 如果用户在短时间内连续进行了几次更改,我希望它等待最后一次更改之后的1秒钟,然后重新生成3d数据。

Is there a "right" way to do this in C#? 在C#中是否有“正确”的方法来做到这一点? I'm assuming, perhaps incorrectly, that accomplishing this will require a second thread waiting and watching a shared DateTime object, then attempting to invoke a method on the original thread when a certain amount of time has passed since the last time Is3dViewValid was set to false. 我以为可能是错误的假设,要完​​成此操作,将需要第二个线程等待并观看共享的DateTime对象,然后在自上次将Is3dViewValid设置为上次以来经过一定时间后,尝试在原始线程上调用方法。假。 Is this right, or is there a better way to do it? 这是对的,还是有更好的方法呢?

Edit: I didn't think of this while I was writing the original question, but what I'm trying to accomplish is that if the event is fired at t=0.1s, t=0.2s, and t=0.3s, I want the regenerate method to run once at t=1.3s 编辑:我在写原始问题时没有想到这一点,但是我要完成的是,如果事件在t = 0.1s,t = 0.2s和t = 0.3s触发,我希望重新生成方法在t = 1.3s时运行一次

I'd start by looking into the Reactive Extensions for .NET, a framework for working with a stream of events (typically user interaction events in the examples) and then manipulating those events as a stream. 我将从研究.NET的Reactive Extensions开始,该框架是用于处理事件流(示例中通常是用户交互事件)的框架,然后将这些事件作为流进行处理。 The most useful thing to you will be the http://msdn.microsoft.com/en-us/library/hh229400(v=vs.103).aspx Observable.Throttle method on the stream which will allow you to specify an amount of time to wait before propagating any change into the rest of the stream where you can take action on it through event subscription. 对您来说最有用的是流上的http://msdn.microsoft.com/zh-cn/library/hh229400(v=vs.103).aspx Observable.Throttle方法,该方法可让您指定金额在将任何更改传播到流的其余部分之前需要等待的时间,您可以在其中通过事件订阅对其进行操作。

Further Resources: 更多资源:

Listed here is a more concise way of expressing the same thing as previously written but also including execution of the needed code asynchronously. 这里列出的是一种更简洁的方式来表达与先前编写的内容相同的内容,但还包括异步执行所需的代码。

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

If you're using .NET 4.5, you can do this in your event handler: 如果使用的是.NET 4.5,则可以在事件处理程序中执行以下操作:

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

Task.Delay will create a Task that does absolutely nothing except for wait a certain amount of time before completing. Task.Delay将创建一个完全不执行任何Task ,除了等待一定时间才能完成。 ContinueWith(Action<Task> action) means "when this task is done, only then do this second task, specified by this action . ContinueWith(Action<Task> action)表示“完成此任务后,才执行此action指定的第二项任务。

If you're additionally using C# 5.0, then you can potentially use the async / await tools and an async event handler to do await Task.Delay(TimeSpan.FromSeconds(5)); 如果您还使用C#5.0,则可以潜在地使用async / await工具和async事件处理程序来执行await Task.Delay(TimeSpan.FromSeconds(5)); followed by the code you want delayed. 然后是您要延迟的代码。


I second Norman H's suggestion to investigate the Reactive Extensions. 我赞同Norman H的建议来研究反应性扩展。 They're a really powerful toolset, but it might be more than you're looking for. 它们是一个非常强大的工具集,但可能超出您的期望。

Ok, I've done some research on Reactive Extensions, I've put together code, and I have two potential answers. 好的,我已经对Reactive Extensions进行了一些研究,将代码放在一起,并且有两个潜在的答案。 I don't know if one of these is the "right" way, but both seem to work. 我不知道这些方法之一是否是“正确”的方法,但两者似乎都可行。 RX seems very powerful, but using it stretches the limits of my C# abilities. RX似乎非常强大,但是使用它可以扩展我的C#能力的局限性。

1. The Problem 1.问题

First, to restate the problem, imagine I have this class: 首先,要重述该问题,假设我有此类:

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

It holds data and it's NOT thread safe. 它保存数据,并且不是线程安全的。 It has three methods. 它有三种方法。 One performs a hefty computation based on its internal data to update itself whenever the data changes. 人们会根据其内部数据进行大量计算,以在数据更改时进行自我更新。 This is accomplished by having internal properties (not demonstrated) that call the InvalidateMyself() function, firing an event to be handled by a parent class, which will at some point decide to tell the object to update itself. 这是通过具有调用InvalidateMyself()函数的内部属性(未演示),触发要由父类处理的事件来实现的,该事件将在某个时候决定告诉对象自己进行更新。 Finally, I have a "simulate user input" method which calls InvalidateMyself() at t=0.1s, t=0.2s, and t=0.3s. 最后,我有一个“模拟用户输入”方法,该方法在t = 0.1s,t = 0.2s和t = 0.3s时调用InvalidateMyself()。

Now, the easiest way to make this class update itself is to take the parent object, an application presenter in this case, listen for the Invalidate event and directly fire the HeftyComputation() method when it comes. 现在,进行此类自身更新的最简单方法是获取父对象(在这种情况下为应用程序演示者),侦听Invalidate事件,并在出现时直接触发HeftyComputation()方法。 Observe the following class: 遵守以下课程:

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

This essentially does that, except that it runs the HeftyComputation() method every time the object is "invalidated". 除了在每次对象“无效”时都运行HeftyComputation()方法外,这基本上是这样做的。 Here's the output: 这是输出:

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

Fair enough? 很公平? Now let's say the behavior I want is for the application presenter to wait for a 1s period of time with no invalidations before performing HeftyComputation(). 现在让我们说我想要的行为是让应用程序演示者在执行HeftyComputation()之前等待1秒钟的时间而没有任何无效。 This way all three updates are handled at one time at t=1.3s. 这样,所有三个更新将在t = 1.3s处一次处理。

I've done this two ways, once using a listening thread and the System.Windows.Threading Dispatcher, and once using Reactive Extensions' IObservable.Throttle 我已经完成了这两种方法,一次使用侦听线程和System.Windows.Threading Dispatcher,一次使用Reactive Extensions的IObservable.Throttle

2. Solution using Tasks and System.Windows.Dispatcher 2.使用Tasks和System.Windows.Dispatcher的解决方案

The only advantage I can think of to using a background Task and the dispatcher is that you're not relying on any third party libraries. 我可以想到的是使用后台Task和分派器的唯一好处是您不必依赖任何第三方库。 You are, however, locked into referencing the WindowsBase assembly, so I can't imagine this is going to work on Mono, if that's important to you. 但是,您被锁定引用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();
    }
}

Here's how it works: when created the application presenter spawns a Task which runs in the background. 它是这样工作的:创建应用程序时,演示者会生成一个在后台运行的Task。 This task watches a bool that represents whether the Project object is still valid and a DateTime that represents the time of the last invalidation. 此任务监视表示项目对象是否仍然有效的布尔值和表示最后一次失效时间的DateTime。 When the bool is false and the last invalidation was more than 1 second ago, the Task invokes a method on the main thread using the Dispatcher which performs HeftyComputation(). 当bool为false且最后一次失效时间超过1秒之前,任务将使用执行HeftyComputation()的Dispatcher在主线程上调用一个方法。 The invalidation event handler now causes the main thread to simply update the bool and the DateTime and wait for the background thread to decide when to run the update. 失效事件处理程序现在使主线程简单地更新bool和DateTime,并等待后台线程决定何时运行更新。

The output: 输出:

Running HeftyComputation() at 1.3060747s

So this seems to work. 因此,这似乎可行。 It's probably not the best way, and I'm not great at concurrency, so if someone sees a mistake or a problem here please point it out. 这可能不是最好的方法,并且我并不擅长并发,因此如果有人在这里看到错误或问题,请指出。

3. Solution using Reactive Extensions 3.使用反应性扩展的解决方案

Here's the solution using Reactive Extensions. 这是使用反应式扩展的解决方案。 Fortunately, NuGet made adding it to the project trivial, but using it was a different story for someone of my skill level. 幸运的是,NuGet可以轻松地将其添加到项目中,但是对于我这样熟练的人来说,使用它是一个不同的故事。 Normon H mentioned it would only be a few lines of code, and that turned out to be true, but getting those few lines of code right took me much longer than writing the Dispatcher solution. Normon H提到只有几行代码,事实证明这是事实,但是正确编写这几行代码比编写Dispatcher解决方案花了我更多的时间。

If you're good with delegates and lambdas Reactive Extensions looks amazing. 如果您对委托和lambda感到满意,那么Reactive Extensions看起来很棒。 I'm not, though, so I struggled. 我不是,所以我挣扎了。

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

And that's it, three lines. 就是这样,三行。 I create an IObservable source from the Project.Invalidated event, create a second IObservable source that throttles the first one, and then subscribe to it so that my UpdateProject() method is called when the throttled source activates. 我从Project.Invalidated事件创建了一个IObservable源,创建了一个第二个IObservable源,它限制了第一个IObservable源,然后订阅了该源,以便在激活被抑制的源时调用我的UpdateProject()方法。 Figuring out how to properly call the Observable.FromEvent method with my event was the hardest part of the process. 弄清楚如何正确地对我的事件调用Observable.FromEvent方法是该过程中最难的部分。

The output: 输出:

Running HeftyComputation() at 1.3090749s

From reading this website: http://www.introtorx.com/uat/content/v1.0.10621.0/13_SchedulingAndThreading.html my takeaway is that while RX uses threads for timing and scheduling, it doesn't by default invoke code on a different thread. 通过阅读该网站: http : //www.introtorx.com/uat/content/v1.0.10621.0/13_SchedulingAndThreading.html我的收获是,尽管RX使用线程进行计时和调度,但默认情况下它不会在一个不同的线程。 Thus the UpdateProject() method should be running on the main thread. 因此,UpdateProject()方法应在主线程上运行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 运行一个使用主线程中的方法的单独线程是否仍然在主线程上运行它? (C#) - Will running a separate thread that uses a method from the main thread still run it on the main thread? (C#) c# 事件的正确实现方式是什么 - What is the right way of implementation c# event 如何在X时间内运行C#任务或线程 - How to run a C# task or thread for a X amount of Time 在设置的时间量和/或中止线程问题后调用方法 - Calling a method after set amount of time and/or aborting thread issues 有没有办法让某事花费一定的时间 c# - Is there a way to make something take a set amount of time c# Windows上的C#在程序仍在运行时更新程序 - c# on windows updating a program while it's still running 在多个请求触发到应用程序服务器后,C#允许一个线程一次运行该方法 - C# allowing one thread to run the method at a time after multiple requests triggerred to application server 线程仍在运行或在C#中终止了异常 - Thread is still running or terminated exception in C# 强制事件处理程序在对象的线程C#.NET上运行 - Force event handler to run on object's thread, C# .NET 在C#中获取Flash文件运行时间的最可靠方法是什么? - What's the most reliable way to get a Flash file's run time in C#?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM