使用Rx限制X并发任务

时间:2017-08-02 11:01:50

标签: c# .net concurrency task-parallel-library system.reactive

我对Rx很新,我试图使用它来获得多达X个并发订阅任务。数据源实际上来自数据库,所以我不得不轮询数据库。我意识到Rx背后的想法是它推送而不是 pull - 所以民意调查并不是那么合适,但概念上数据进入数据库是我要订阅并执行某些操作的事件流。

我遇到的主要问题是LimitedConcurrencyLevelTaskScheduler似乎没有成功限制到指定数量的任务。它比我指定的8个并发运行的更多。

我也不确定哪种方法更适合以下两种解决方案(或者可能两种方法都错了?!)。

这是我试过的一种使用Observable.Timer的方法...

public static void Main()
{
    var taskFactory = new TaskFactory (new LimitedConcurrencyLevelTaskScheduler (8));
    var scheduler = new TaskPoolScheduler (taskFactory);

    Observable.Timer (TimeSpan.FromMilliseconds (10), scheduler)
        .SelectMany (x => Observable.FromAsync (GetItemsSource))
        .Repeat ()
        .ObserveOn (scheduler)
        .Subscribe (x => Observable.FromAsync(y => DoSomethingAsync (x.ToList())));

    Console.ReadKey ();
}

private static async Task<IEnumerable<Guid>> GetItemsSource()
{
    return await _myRepo.GetMoreAsync(10);
}

private static async Task DoSomethingAsync(IEnumerable<Guid> items)
{
    // Do something with the data
}

我也试过这样做而不是......

public static void Main()
{
    GetItemsSource()
        .ObserveOn(scheduler)
        .Select (async x => await DoSomethingAsync(x))
        .Subscribe();

    Console.ReadKey ();
}

public static IObservable<Guid> GetItemsSource()
{
    return Observable.Create<Guid>(
        async obs =>
        {
            while (true)
            {
                var item = (await _myRepo.GetMoreAsync(1)).FirstOrDefault();

                if(item != null)
                {
                    obs.OnNext(item);
                }

                await Task.Delay(TimeSpan.FromMilliseconds(10))
            }
        });
}

private static async Task DoSomethingAsync(IEnumerable<Guid> items)
{
    // Do something with the data
}

显然是非常简单的示例,没有错误处理或取消支持。

两者似乎都有效,但不限于8个并发任务。

正如我所说,我对Rx很新,可能会遗漏很多基本的东西。我当然打算做大量阅读以完全理解Rx,因为它看起来非常强大,但是现在我希望能够快速完成某些工作。

更新

根据Enigmativity的回答和评论,这里有一些代码可以记录并发计数...

void Main()
{
    var taskFactory = new TaskFactory(new LimitedConcurrencyLevelTaskScheduler(8));
    var scheduler = new TaskPoolScheduler(taskFactory);

    using (
        (
            from n in Observable.Interval(TimeSpan.FromMilliseconds(10), scheduler)
            from g in Observable.FromAsync(GetItemsSource, scheduler)
            from u in Observable.FromAsync(() => DoSomethingAsync(g), scheduler)
            select u)
        .ObserveOn(scheduler)
        .Subscribe())
    {
        Console.ReadLine();
    }
}

private static volatile int _numIn = 0;
private static volatile int _numOut = 0;

public static async Task<IEnumerable<Guid>> GetItemsSource()
{
    try
    {
        _numIn++;

        $"Concurrent tasks (in): {_numIn}".Dump();

        // Simulate async API call
        await Task.Delay(TimeSpan.FromMilliseconds(10));

        return new List<Guid> { Guid.NewGuid() };
    }
    finally
    {
        _numIn--;
    }
}

private static async Task DoSomethingAsync(IEnumerable<Guid> deliveryIds)
{
    try
    {
        _numOut++;

        // Simulate async calls required to process the event
        await Task.Delay(TimeSpan.FromMilliseconds(1000));

        $"Concurrent tasks (out): {_numOut}".Dump();
    }
    finally
    {
        _numOut--;
    }
}

这表明大约有64个并发任务正在运行。

更新2

看起来这是因为订阅者是异步的。如果我使用非异步订阅者进行测试,它可以正常工作。不幸的是,我需要一个异步订阅者,因为它需要调用其他异步方法。

看起来我可以通过这样做来做类似的事情......

GetItemsSource2()
    .Select(x => Observable.FromAsync(() => DoSomethingAsync(x)))
    .Merge(64)
    .Subscribe();

因此使用Merge代替LimitedConcurrencyLevelTaskScheduler

1 个答案:

答案 0 :(得分:1)

我通过这样做了你的代码:

void Main()
{
    var taskFactory = new TaskFactory(new LimitedConcurrencyLevelTaskScheduler(8));
    var scheduler = new TaskPoolScheduler(taskFactory);

    using (Observable.Timer(TimeSpan.FromMilliseconds(10), scheduler)
        .SelectMany(x => Observable.FromAsync(GetItemsSource))
        .Repeat()
        .ObserveOn(scheduler)
        .Subscribe(async x => await DoSomethingAsync(x.ToList())))
    {
        Console.ReadLine();
    };
}

private static async Task<IEnumerable<Guid>> GetItemsSource()
{
    return await Task.Run(() => Enumerable.Range(0, 10).Select(x => Guid.NewGuid()).ToArray());
}

private static async Task DoSomethingAsync(IEnumerable<Guid> items)
{
    await Task.Run(() => Console.WriteLine(String.Join("|", items.Select(x => x.ToString()))));
}

我还修改了QueueTask的{​​{1}}方法,以包含正在运行的委托数量的跟踪线:

LimitedConcurrencyLevelTaskScheduler

当我运行你的代码时,我得到了这个输出:

0
1
1
1
874695ca-e9a8-4688-a4d2-d7d2446e7924|e3cbadf1-c5cb-4339-ab59-33d873db98f2|8dd710f5-0b21-4b20-a547-6ccfe7c29af4|42739c5a-4602-4f76-8aed-40f1c0ffccdc|c599e879-06ca-459f-b27e-95ac15dc4cc1|562b5f7c-dcb2-47d8-aa4a-503499654139|b78e5fc5-d152-4380-9799-2713c6f71c19|e47669a3-a399-4891-91b6-3a28b52a941a|f6483f2f-f8d7-47e8-9f88-bf9bc5f61f3a|c8e75203-bc55-4e00-9f8b-ecf248f81454
2
2
1
2
3
92d6f76e-6a3d-475d-8c1c-bd3e59aaf2a1|1b2bd0d6-c439-4b3c-a1f1-9af1f5132afc|c140e0ec-6741-4310-9edf-547fdf390b01|5b29dcd3-21c9-46a2-ae98-cd7a7b1a003c|5a808def-a09f-41a7-acfc-d11cfbbb4faa|28f47d0c-7762-4949-ae33-427c82756874|13087b1c-c4eb-4f0f-bf5f-665a2298ac13|50c00907-668d-44f3-9e2a-c790348fb715|34f8602a-18ef-45b6-b069-0fb30718a45b|46d6616d-0e89-49cd-8905-dff72bb63add
2
1
1
2
7c6cbe5a-43d1-4aad-8eee-33a0d7f44276|2164e3e0-4e8d-4a57-99b7-0aa5e42281da|e2cf26ed-501f-4032-9761-53a301e16bb9|a3e9171a-f490-4135-a930-017dc706293e|b9f43f5c-d652-4205-b857-724699f178c5|5d8e6149-f9ad-424d-9ee2-07afa344ab80|418d2526-adf6-4c26-ac84-636400fce547|3b8c3b14-9e91-4bb8-9cfa-c1d36f12ccc0|be3c8c84-5112-4c85-ba7f-d1b41a2ec03a|bdc34ccb-a3d9-45fa-bf54-d42bcd791081
2
1
1
2
3d5e0d0a-ddb3-4595-b960-4d2050d4fa1a|1b5a39c9-652c-4872-8d1c-6e1212cd4043|fd9aff7b-9c77-47e4-a4c6-2ede38472b92|ff6145d3-2dc2-45b6-bc40-2cc1f270572d|5ba0e441-a9f1-4b2a-baca-127cce622993|e2650bb9-f2e4-4a89-8c2c-69859f5f381a|a86a1f4e-7ea7-465c-9730-7ac735c5616e|5cf7135b-fafe-4725-938e-5e7ea55f4c3e|dd26bda7-d86b-4bb9-977f-a29ef5cf6d76|6e77c1c1-4e8d-4b48-b6f4-d54f0ec9d269
1
1

...等...

您的代码永远不会超过3个并发任务。

现在我建议您尝试以更多Rx-ish方式编写代码。试试这个:

protected sealed override void QueueTask(Task task)
{
    // Add the task to the list of tasks to be processed.  If there aren't enough 
    // delegates currently queued or running to process tasks, schedule another. 
    lock (_tasks)
    {
        Console.WriteLine(_delegatesQueuedOrRunning);
        _tasks.AddLast(task);
        if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)
        {
            ++_delegatesQueuedOrRunning;
            NotifyThreadPoolOfPendingWork();
        }
    }
}

它不会改变使用的任务数量,但它更清洁。