我有一种方法可以从服务器中提取数据并将其返回进行处理。我做了一些测量,发现在后台下载块并通过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库)应该知道发生了异常,因此它可以再次尝试或酌情进行纾困。将异常传播给调用者的最佳方法是什么,同时最小化更改返回类型?
答案 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;
}
}