11

我正在开发一个模拟系统,除其他外,它允许以离散的模拟时间步长执行任务。执行都发生在模拟线程的上下文中,但是,从使用系统的“操作员”的角度来看,他们希望以异步方式运行。值得庆幸的是,带有方便的“async/await”关键字的 TPL 使这变得相当简单。我在模拟上有一个原始方法,如下所示:

    public Task CycleExecutedEvent()
    {
        lock (_cycleExecutedBroker)
        {
            if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped");
            return _cycleExecutedBroker.RegisterForCompletion(CycleExecutedEventName);
        }
    }

这基本上是创建一个新的 TaskCompletionSource,然后返回一个 Task。此任务的目的是在模拟中出现新的“ExecuteCycle”时执行其延续。

然后我有一些这样的扩展方法:

    public static async Task WaitForDuration(this ISimulation simulation, double duration)
    {
        double startTime = simulation.CurrentSimulatedTime;
        do
        {
            await simulation.CycleExecutedEvent();
        } while ((simulation.CurrentSimulatedTime - startTime) < duration);
    }

    public static async Task WaitForCondition(this ISimulation simulation, Func<bool> condition)
    {
        do
        {
            await simulation.CycleExecutedEvent();
        } while (!condition());
    }

因此,这些对于从“操作员”的角度构建序列、根据条件采取行动并等待模拟时间段非常方便。我遇到的问题是 CycleExecuted 非常频繁地发生(如果我以完全加速的速度运行,大约每隔几毫秒)。因为这些“等待”辅助方法在每个循环中注册一个新的“等待”,这会导致 TaskCompletionSource 实例的大量周转。

我分析了我的代码,发现大约 5.5% 的总 CPU 时间花费在这些完成中,其中只有一小部分花费在“活动”代码中。实际上,在等待触发条件有效时,所有时间都花在注册新的完成上。

我的问题:我怎样才能在这里提高性能,同时仍然保留异步/等待模式编写“操作员行为”的便利性?考虑到触发事件如此频繁地发生,我想我需要一个轻量级和/或可重用的 TaskCompletionSource 之类的东西。


我一直在做更多的研究,听起来一个不错的选择是创建 Awaitable 模式的自定义实现,它可以直接绑定到事件中,从而无需一堆 TaskCompletionSource 和 Task 实例。它在这里有用的原因是有很多不同的延续在等待 CycleExecutedEvent 并且他们需要经常等待它。所以理想情况下,我正在寻找一种方法来排队继续回调,然后在事件发生时回调队列中的所有内容。我会继续挖掘,但如果人们知道一种干净的方法,我欢迎任何帮助。


对于将来浏览此问题的任何人,这是我整理的自定义等待者:

public sealed class CycleExecutedAwaiter : INotifyCompletion
{
    private readonly List<Action> _continuations = new List<Action>();

    public bool IsCompleted
    {
        get { return false; }
    }

    public void GetResult()
    {
    }

    public void OnCompleted(Action continuation)
    {
        _continuations.Add(continuation);
    }

    public void RunContinuations()
    {
        var continuations = _continuations.ToArray();
        _continuations.Clear();
        foreach (var continuation in continuations)
            continuation();
    }

    public CycleExecutedAwaiter GetAwaiter()
    {
        return this;
    }
}

在模拟器中:

    private readonly CycleExecutedAwaiter _cycleExecutedAwaiter = new CycleExecutedAwaiter();

    public CycleExecutedAwaiter CycleExecutedEvent()
    {
        if (!IsRunning) throw new TaskCanceledException("Simulation has been stopped");
        return _cycleExecutedAwaiter;
    }

这有点有趣,因为等待者从不报告 Complete,但 fires 在注册时继续调用完成;尽管如此,它仍然适用于这个应用程序。这将 CPU 开销从 5.5% 降低到 2.1%。它可能仍需要进行一些调整,但与原始版本相比,这是一个很好的改进。

4

3 回答 3

9

await关键字不仅适用于Tasks,它适用于任何遵循等待模式的东西。有关详细信息,请参阅Stephen Toub 的文章等待任何东西;.

简短的版本是该类型必须有一个方法,该方法GetAwaiter()返回一个实现INotifyCompletion并且还具有IsCompleted属性和GetResult()方法的类型(void-returning,如果await表达式不应该有值)。例如,请参阅TaskAwaiter

如果您创建自己的等待对象,则每次都可以返回相同的对象,从而避免分配许多TaskCompletionSources 的开销。

于 2012-06-27T19:37:25.077 回答
5

这是我的ReusableAwaiter模拟版本TaskCompletionSource

public sealed class ReusableAwaiter<T> : INotifyCompletion
{
    private Action _continuation = null;
    private T _result = default(T);
    private Exception _exception = null;

    public bool IsCompleted
    {
        get;
        private set;
    }

    public T GetResult()
    {
        if (_exception != null)
            throw _exception;
        return _result;
    }

    public void OnCompleted(Action continuation)
    {
        if (_continuation != null)
            throw new InvalidOperationException("This ReusableAwaiter instance has already been listened");
        _continuation = continuation;
    }

    /// <summary>
    /// Attempts to transition the completion state.
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    public bool TrySetResult(T result)
    {
        if (!this.IsCompleted)
        {
            this.IsCompleted = true;
            this._result = result;

            if (_continuation != null)
                _continuation();
            return true;
        }
        return false;
    }

    /// <summary>
    /// Attempts to transition the exception state.
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    public bool TrySetException(Exception exception)
    {
        if (!this.IsCompleted)
        {
            this.IsCompleted = true;
            this._exception = exception;

            if (_continuation != null)
                _continuation();
            return true;
        }
        return false;
    }

    /// <summary>
    /// Reset the awaiter to initial status
    /// </summary>
    /// <returns></returns>
    public ReusableAwaiter<T> Reset()
    {
        this._result = default(T);
        this._continuation = null;
        this._exception = null;
        this.IsCompleted = false;
        return this;
    }

    public ReusableAwaiter<T> GetAwaiter()
    {
        return this;
    }
}

这是测试代码。

class Program
{
    static readonly ReusableAwaiter<int> _awaiter = new ReusableAwaiter<int>();

    static void Main(string[] args)
    {
        Task.Run(() => Test());

        Console.ReadLine();
        _awaiter.TrySetResult(22);
        Console.ReadLine();
        _awaiter.TrySetException(new Exception("ERR"));

        Console.ReadLine();
    }

    static async void Test()
    {

        int a = await AsyncMethod();
        Console.WriteLine(a);
        try
        {
            await AsyncMethod();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

    }

    static  ReusableAwaiter<int> AsyncMethod()
    {
        return _awaiter.Reset();
    }

}
于 2016-11-19T04:26:51.583 回答
0

你真的需要WaitForDuration在不同的线程上接收 -event 吗?如果没有,您可以只注册一个回调(或事件)_cycleExecutedBroker并同步接收通知。在回调中,您可以测试您喜欢的任何条件,并且只有当该条件结果为真时,才通知不同的线程(使用任务或消息或任何机制)。我了解您测试的条件很少评估为真,因此您可以避免大多数跨线程调用。

我想我的回答的要点是:尝试通过将计算移动到“源”线程来减少跨线程消息传递的数量。

于 2012-06-27T18:51:35.047 回答