C#异步聚合和调度

C#异步聚合和调度,c#,async-await,C#,Async Await,我在吸收c#任务、异步和等待模式时遇到了困难 Windows服务.NET v4.5.2服务器端 我有一个Windows服务,它接受各种传入记录的来源,通过自托管的web api从外部临时到达。我想对这些记录进行批处理,然后将它们转发到其他服务。如果批处理记录的数量超过阈值,则应立即分派批处理。此外,如果时间间隔已过,则还应调度现有批次。这意味着记录的保存时间永远不会超过N秒 我正在努力将其融入基于任务的异步模式 在过去的日子里,我会创建一个线程、一个ManualResetEvent和一个Syst

我在吸收c#任务、异步和等待模式时遇到了困难

Windows服务.NET v4.5.2服务器端

我有一个Windows服务,它接受各种传入记录的来源,通过自托管的web api从外部临时到达。我想对这些记录进行批处理,然后将它们转发到其他服务。如果批处理记录的数量超过阈值,则应立即分派批处理。此外,如果时间间隔已过,则还应调度现有批次。这意味着记录的保存时间永远不会超过N秒

我正在努力将其融入基于任务的异步模式

在过去的日子里,我会创建一个线程、一个ManualResetEvent和一个System.Threading.Timer。线程将循环等待重置事件。计时器将在触发时设置事件,当批大小超过阈值时进行聚合的代码也会设置事件。在等待之后,线程将停止计时器、执行分派(HTTP Post)、重置计时器并清除ManualResetEvent、循环并等待

然而,我看到人们说这是“不好的”,因为等待只是阻塞了一个有价值的线程资源,而异步/等待是我的灵丹妙药

首先,他们说得对吗?我的方式是否过时且效率低下,或者我可以使用JFDI

我已经找到了批处理和间隔任务的示例,但不是两者的组合


这项要求实际上与async/await兼容吗?

事实上,您所做的几乎是正确的,而且它们也部分正确

您应该知道的是,您应该避免空闲线程,长时间等待事件或等待I/O完成(等待具有少量争用和快速语句块的锁,或者使用比较和交换旋转循环通常是可以的)

他们中的大多数人不知道的是,任务不是魔术,例如,(更确切地说,是
系统.Threading.Timer
)和(相对于
手动重置事件的改进,因为除非明确要求,否则它不会创建Win32事件,例如
((IAsyncResult)任务).AsyncWaitHandle

因此,您的需求可以通过async/await或一般任务来实现

:

使用系统;
使用System.Collections.Generic;
使用系统诊断;
使用系统线程;
使用System.Threading.Tasks;
公开课记录
{
私人int n;
公共记录(int n)
{
这个,n=n;
}
公共int N{get{return N;}}
}
公共类录音机
{
//任意常数
//您应该从配置中获取值并定义合理的默认值
私有静态只读int阈值=5;
//我选择了一个较低的值,这样示例就不会在.NET Fiddle中超时
私有静态只读TimeSpan超时=TimeSpan.FromMillics(100);
//我会用秒表来追踪执行时间
私有只读秒表sw=Stopwatch.StartNew();
//使用单独的私有对象进行锁定
私有只读对象lockObj=新对象();
//要批量执行的累积记录列表
私有列表记录=新列表();
//在以下情况下,最新的TCS将发出完成信号:
//-列表计数已达到阈值
//-足够的时间过去了
私有任务完成源批处理;
//达到阈值时取消基于计时器的任务的CTS
//严格来说这不是必需的,但它减少了资源的使用
私有取消令牌源延迟;
//发送一批记录后将完成的任务
私有任务调度任务;
//此方法不使用async/await,
//因为我们这里不是在做异步流。
公共任务接收异步(记录)
{
WriteLine(“接收到的记录{0}({1})”,record.N,sw.elapsedmillesons);
锁(lockObj)
{
//当记录列表为空时,设置下一个任务
//
//TaskCompletionSource正是我们需要的,我们将完成一项任务
//不是当我们完成一些计算,而是当我们达到一些标准时
//
//这是此方法不使用async/await的主要原因
如果(records.Count==0)
{
//我希望调度任务在线程池上运行
//在.NET 4.6中,有TaskCreationOptions.RunContinuationsA同步运行
//.NET 4.6
//batchTcs=新TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
//dispatchTask=DispatchRecordsAsync(batchTcs.Task);
//以前,我们必须使用默认任务调度器设置一个延续任务
//.NET 4.5.2
batchTcs=新任务完成源();
var asyncContinuationsTask=batchTcs.Task
.ContinueWith(bt=>bt.Result,TaskScheduler.Default);
dispatchTask=DispatchRecordsAsync(asyncContinuationsTask);
//创建一个取消令牌源,以便能够取消计时器
//
//在达到阈值时使用,释放计时器资源
delayCts=新的CancellationTokenSource();
Task.Delay(超时,delayCts.Token)
.继续(
dt=>
{
//当我们按下计时器时,打开锁并设置批次
//任务完成后,将当前记录移动到其结果
锁(lockObj)
{
//避免发送空的记录列表
//
//还可以通过检查取消令牌来避免竞争条件
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

public class Record
{
    private int n;

    public Record(int n)
    {
        this.n = n;
    }

    public int N { get { return n; } }
}

public class RecordReceiver
{
    // Arbitrary constants
    // You should fetch value from configuration and define sensible defaults
    private static readonly int threshold = 5;
    // I chose a low value so the example wouldn't timeout in .NET Fiddle
    private static readonly TimeSpan timeout = TimeSpan.FromMilliseconds(100);

    // I'll use a Stopwatch to trace execution times
    private readonly Stopwatch sw = Stopwatch.StartNew();
    // Using a separate private object for locking
    private readonly object lockObj = new object();
    // The list of accumulated records to execute in a batch
    private List<Record> records = new List<Record>();
    // The most recent TCS to signal completion when:
    // - the list count reached the threshold
    // - enough time has passed
    private TaskCompletionSource<IEnumerable<Record>> batchTcs;
    // A CTS to cancel the timer-based task when the threshold is reached
    // Not strictly necessary, but it reduces resource usage
    private CancellationTokenSource delayCts;
    // The task that will be completed when a batch of records has been dispatched
    private Task dispatchTask;

    // This method doesn't use async/await,
    // because we're not doing an async flow here.
    public Task ReceiveAsync(Record record)
    {
        Console.WriteLine("Received record {0} ({1})", record.N, sw.ElapsedMilliseconds);
        lock (lockObj)
        {
            // When the list of records is empty, set up the next task
            //
            // TaskCompletionSource is just what we need, we'll complete a task
            // not when we've finished some computation, but when we reach some criteria
            //
            // This is the main reason this method doesn't use async/await
            if (records.Count == 0)
            {
                // I want the dispatch task to run on the thread pool

                // In .NET 4.6, there's TaskCreationOptions.RunContinuationsAsynchronously
                // .NET 4.6
                //batchTcs = new TaskCompletionSource<IEnumerable<Record>>(TaskCreationOptions.RunContinuationsAsynchronously);
                //dispatchTask = DispatchRecordsAsync(batchTcs.Task);

                // Previously, we have to set up a continuation task using the default task scheduler
                // .NET 4.5.2
                batchTcs = new TaskCompletionSource<IEnumerable<Record>>();
                var asyncContinuationsTask = batchTcs.Task
                    .ContinueWith(bt => bt.Result, TaskScheduler.Default);
                dispatchTask = DispatchRecordsAsync(asyncContinuationsTask);

                // Create a cancellation token source to be able to cancel the timer
                //
                // To be used when we reach the threshold, to release timer resources
                delayCts = new CancellationTokenSource();
                Task.Delay(timeout, delayCts.Token)
                    .ContinueWith(
                        dt =>
                        {
                            // When we hit the timer, take the lock and set the batch
                            // task as complete, moving the current records to its result
                            lock (lockObj)
                            {
                                // Avoid dispatching an empty list of records
                                //
                                // Also avoid a race condition by checking the cancellation token
                                //
                                // The race would be for the actual timer function to start before
                                // we had a chance to cancel it
                                if ((records.Count > 0) && !delayCts.IsCancellationRequested)
                                {
                                    batchTcs.TrySetResult(new List<Record>(records));
                                    records.Clear();
                                }
                            }
                        },
                        // Since our continuation function is fast, we want it to run
                        // ASAP on the same thread where the actual timer function runs
                        //
                        // Note: this is just a hint, but I trust it'll be favored most of the time
                        TaskContinuationOptions.ExecuteSynchronously);
                // Remember that we want our batch task to have continuations
                // running outside the timer thread, since dispatching records
                // is probably too much work for a timer thread.
            }
            // Actually store the new record somewhere
            records.Add(record);
            // When we reach the threshold, set the batch task as complete,
            // moving the current records to its result
            //
            // Also, cancel the timer task
            if (records.Count >= threshold)
            {
                batchTcs.TrySetResult(new List<Record>(records));
                delayCts.Cancel();
                records.Clear();
            }
            // Return the last saved dispatch continuation task
            //
            // It'll start after either the timer or the threshold,
            // but more importantly, it'll complete after it dispatches all records
            return dispatchTask;
        }
    }

    // This method uses async/await, since we want to use the async flow
    internal async Task DispatchRecordsAsync(Task<IEnumerable<Record>> batchTask)
    {
        // We expect it to return a task right here, since the batch task hasn't had
        // a chance to complete when the first record arrives
        //
        // Task.ConfigureAwait(false) allows us to run synchronously and on the same thread
        // as the completer, but again, this is just a hint
        //
        // Remember we've set our task to run completions on the thread pool?
        //
        // With .NET 4.6, completing a TaskCompletionSource created with
        // TaskCreationOptions.RunContinuationsAsynchronously will start scheduling
        // continuations either on their captured SynchronizationContext or TaskScheduler,
        // or forced to use TaskScheduler.Default
        //
        // Before .NET 4.6, completing a TaskCompletionSource could mean
        // that continuations ran withing the completer, especially when
        // Task.ConfigureAwait(false) was used on an async awaiter, or when
        // Task.ContinueWith(..., TaskContinuationOptions.ExecuteSynchronously) was used
        // to set up a continuation
        //
        // That's why, before .NET 4.6, we need to actually run a task for that effect,
        // and we used Task.ContinueWith without TaskContinuationOptions.ExecuteSynchronously
        // and with TaskScheduler.Default, to ensure it gets scheduled
        //
        // So, why am I using Task.ConfigureAwait(false) here anyway?
        // Because it'll make a difference if this method is run from within
        // a Windows Forms or WPF thread, or any thread with a SynchronizationContext
        // or TaskScheduler that schedules tasks on a dedicated thread
        var batchedRecords = await batchTask.ConfigureAwait(false);
        // Async methods are transformed into state machines,
        // much like iterator methods, but with async specifics
        //
        // What await actually does is:
        // - check if the awaitable is complete
        //   - if so, continue executing
        //     Note: if every awaited awaitable is complete along an async method,
        //           the method will complete synchronously
        //           This is only expectable with tasks that have already completed
        //           or I/O that is always ready, e.g. MemoryStream
        //   - if not, return a task and schedule a continuation for just after the await expression
        //     Note: the continuation will resume the state machine on the next state
        //     Note: the returned task will complete on return or on exception,
        //           but that is something the compiled state machine will handle
        foreach (var record in batchedRecords)
        {
            Console.WriteLine("Dispatched record {0} ({1})", record.N, sw.ElapsedMilliseconds);
            // I used Task.Yield as a replacement for actual work
            //
            // It'll force the async state machine to always return here
            // and shedule a continuation that reenters the async state machine right afterwards
            //
            // This is not something you usually want on production code,
            // so please replace this with the actual dispatch
            await Task.Yield();
        }
    }
}

public class Program
{
    public static void Main()
    {
        // Our main entry point is synchronous, so we run an async entry point and wait on it
        //
        // The difference between MainAsync().Result and MainAsync().GetAwaiter().GetResult()
        // is in the way exceptions are thrown:
        // - the former aggregates exceptions, throwing an AggregateException
        // - the latter doesn't aggregate exceptions if it doesn't have to, throwing the actual exception
        //
        // Since I'm not combining tasks (e.g. Task.WhenAll), I'm not expecting multiple exceptions
        //
        // If my main method returned int, I could return the task's result
        // and I'd make MainAsync return Task<int> instead of just Task
        MainAsync().GetAwaiter().GetResult();
    }

    // Async entry point
    public static async Task MainAsync()
    {
        var receiver = new RecordReceiver();
        // I'll provide a few records: 
        // - a delay big enough between the 1st and the 2nd such that the 1st will be dispatched
        // - 8 records in a row, such that 5 of them will be dispatched, and 3 of them will wait
        // - again, a delay big enough that will provoke the last 3 records to be dispatched
        // - and a final record, which will wait to be dispatched
        //
        // We await for Task.Delay between providing records,
        // but we'll await for the records in the end only
        //
        // That is, we'll not await each record before the next,
        // as that would mean each record would only be dispatched after at least the timeout
        var t1 = receiver.ReceiveAsync(new Record(1));
        await Task.Delay(TimeSpan.FromMilliseconds(300));
        var t2 = receiver.ReceiveAsync(new Record(2));
        var t3 = receiver.ReceiveAsync(new Record(3));
        var t4 = receiver.ReceiveAsync(new Record(4));
        var t5 = receiver.ReceiveAsync(new Record(5));
        var t6 = receiver.ReceiveAsync(new Record(6));
        var t7 = receiver.ReceiveAsync(new Record(7));
        var t8 = receiver.ReceiveAsync(new Record(8));
        var t9 = receiver.ReceiveAsync(new Record(9));
        await Task.Delay(TimeSpan.FromMilliseconds(300));
        var t10 = receiver.ReceiveAsync(new Record(10));
        // I probably should have used a list of records, but this is just an example
        await Task.WhenAll(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10);
    }
}