如何处理匿名任务中的异常?

时间:2014-07-09 16:28:06

标签: c# exception-handling task blockingcollection

我有一种方法可以从服务器中提取数据并将其返回进行处理。我做了一些测量,发现在后台下载块并通过BlockingCollection<T>返回它们要快得多。这允许客户端和服务器同时工作,而不是彼此等待。

public static IEnumerable<DataRecord> GetData(String serverAddress, Int64 dataID)
{
    BlockingCollection<DataRecord> records = new BlockingCollection<DataRecord>();

    Task.Run(
        () =>
        {
            Boolean isMoreData = false;
            do
            {
                // make server request and process response
                // this block can throw

                records.Add(response.record);
                isMoreData = response.IsMoreData;
            }
            while (isMoreData);

            records.CompleteAdding();
        });

    return records.GetConsumingEnumerable();
}

调用者(C ++ / CLI库)应该知道发生了异常,因此它可以再次尝试或酌情进行纾困。将异常传播给调用者的最佳方法是什么,同时最小化更改返回类型?

2 个答案:

答案 0 :(得分:2)

这就是为什么一劳永逸的任务通常是一个坏主意。在你的案例中,他们的想法更糟糕,因为你没有在try/catch records.CompleteAdding {{}}} {{}}} {{}}}块中包含finally,这意味着调用了MoveNext来自GetConsumingEnumerable的调查员最终将无限期地阻止 - 这是坏事。

如果您完全在C#的范围内操作,解决方案将很简单:更好地分离关注点。你删除BlockingCollection位并在它所属的位置运行:在消费者(客户端)或中间流水线处理阶段(这最终是你想要实现的),它将以一种方式设计仍然知道生产者抛出的任何例外情况。您的GetData签名保持不变,但它变成了一个简单的阻塞,可以完全异常传播:

public static IEnumerable<DataRecord> GetData(String serverAddress, Int64 dataID)
{
    Boolean isMoreData = false;
    do
    {
        // make server request and process response
        // this block can throw

        yield return response.record;
        isMoreData = response.IsMoreData;
    }
    while (isMoreData);
}

然后管道看起来像这样:

var records = new BlockingCollection<DataRecord>();

var producer = Task.Run(() =>
{
    try
    {
        foreach (var record in GetData("http://foo.com/Service", 22))
        {
            // Hand over the record to the
            // consumer and continue enumerating.
            records.Add(record);
        }
    }
    finally
    {
        // This needs to be called even in
        // exceptional scenarios so that the
        // consumer task does not block
        // indefinitely on the call to MoveNext.
        records.CompleteAdding();
    }
});

var consumer = Task.Run(() =>
{
    foreach (var record in records.GetConsumingEnumerable())
    {
        // Do something with the record yielded by GetData.
        // This runs in parallel with the producer,
        // So you get concurrent download and processing
        // with a safe handover via the BlockingCollection.
    }
});

await Task.WhenAll(producer, consumer);

现在你可以拥有你的蛋糕并吃掉它:处理过程并行发生,因为记录是由GetData产生的,await生成器任务传播任何异常,而调用{{1}在CompleteAdding内部确保您的消费者不会无限期地陷入阻塞状态。

由于你正在使用C ++,上面仍然适用于某种程度(也就是说,正确的做法是在C ++中重新实现管道),但实现可能不那么漂亮,而且你的方式也是如此已经过去了,你的答案很可能是首选的解决方案,即使它因为未被观察到的任务而感觉像是一个黑客。我真的不能想到实际出错的情况,因为finally总是因为新引入的CompleteAdding而被调用。

显然,另一种解决方案是将处理代码移动到您的C#项目,根据您的架构,这可能会也可能不会。

答案 1 :(得分:0)

我发现最简单的解决方案是返回DataResult上下文,在枚举其记录后可能包含异常。

public class DataResult
{
    internal DataResult(IEnumerable<DataRecord> records)
    {
        Records = records;
    }

    public IEnumerable<DataRecord> Records { get; private set; }
    public Exception Exception { get; internal set; }
}

public static DataResult GetData(String serverAddress, Int64 dataID)
{
    BlockingCollection<DataRecord> records = new BlockingCollection<DataRecord>();
    DataResult result = new DataResult(records.GetConsumingEnumerable());

    Task.Run(
        () =>
        {
            try
            {
                Boolean isMoreData = false;
                do
                {
                    // make server request and process response
                    // this block can throw

                    records.Add(response.record);
                    isMoreData = response.IsMoreData;
                }
                while (isMoreData);
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            finally
            {
                records.CompleteAdding();
            }
        });

    return result;
}

如果有异常,调用者(C ++ / CLI)可以重新抛出它。

void Caller()
{
    DataResult^ result = GetData("http://foo.com/Service", 22);

    foreach (DataRecord record in result->Records)
    {
        // process records
    }

    Exception^ ex = result->Exception;
    if (ex != nullptr)
    {
        throw ex;
    }
}