C# 如何使用2个数据源获得异步流返回
我有一个函数,它以异步流的形式返回标准输出数据,该数据是运行C# 如何使用2个数据源获得异步流返回,c#,asynchronous,tpl-dataflow,iasyncenumerable,C#,Asynchronous,Tpl Dataflow,Iasyncenumerable,我有一个函数,它以异步流的形式返回标准输出数据,该数据是运行System.Diagnostics.Process得到的。当前方法中的所有内容都按预期工作;我可以在await foreach()循环中调用它,并获得由外部exe生成的每一行输出 private static async IAsyncEnumerable<string> ProcessAsyncStream ( ProcessStartInfo processStartInfo) { // Ensure th
System.Diagnostics.Process
得到的。当前方法中的所有内容都按预期工作;我可以在await foreach()
循环中调用它,并获得由外部exe生成的每一行输出
private static async IAsyncEnumerable<string> ProcessAsyncStream (
ProcessStartInfo processStartInfo)
{
// Ensure that process is destroyed when this method exits
using var process = new Process() { StartInfo = processStartInfo };
// Buffer used to pass data from event-handler back to this method
BufferBlock<string> dataBuffer = new BufferBlock<string>();
process.OutputDataReceived += (s, e) =>
{
if (e.Data is null)
{
dataBuffer.Complete();
}
else
{
dataBuffer.Post(e.Data);
}
};
// Start process and redirect output streams
process.Start();
process.BeginOutputReadLine();
// Return data line by line
while (await dataBuffer.OutputAvailableAsync())
yield return dataBuffer.Receive();
}
并将ProcessAsyncStream()
更改为如下所示
private static async IAsyncEnumerable<ProcessData> ProcessAsyncStream (
ProcessStartInfo processStartInfo)
{
// Ensure that process is destroyed when this method exits
using var process = new Process() { StartInfo = processStartInfo };
// Buffer used to pass data from event-handlers back to this method
BufferBlock<string> outputDataBuffer = new BufferBlock<string>();
BufferBlock<string> errorDataBuffer = new BufferBlock<string>();
process.OutputDataReceived += (s, e) =>
{
if (e.Data is null)
{
outputDataBuffer.Complete();
}
else
{
outputDataBuffer.Post(e.Data);
}
};
process.ErrorDataReceived += (s, e) =>
{
if (e.Data is null)
{
errorDataBuffer.Complete();
}
else
{
errorDataBuffer.Post(e.Data);
}
};
// Start process and redirect output streams
process.Start();
process.BeginOutputReadLine();
// Return data line by line
while (await outputDataBuffer.OutputAvailableAsync()
|| await errorDataBuffer.OutputAvailableAsync())
yield return new ProcessData()
{
Error = errorDataBuffer.Receive(),
Output = outputDataBuffer.Receive()
}
}
私有静态异步IAsyncEnumerable ProcessAsyncStream(
ProcessStartInfo ProcessStartInfo)
{
//确保此方法退出时销毁进程
使用var process=new process(){StartInfo=processStartInfo};
//用于将数据从事件处理程序传递回此方法的缓冲区
BufferBlock outputDataBuffer=新的BufferBlock();
BufferBlock errorDataBuffer=新的BufferBlock();
process.OutputDataReceived+=(s,e)=>
{
如果(例如,数据为空)
{
outputDataBuffer.Complete();
}
其他的
{
outputDataBuffer.Post(即数据);
}
};
process.ErrorDataReceived+=(s,e)=>
{
如果(例如,数据为空)
{
errorDataBuffer.Complete();
}
其他的
{
errorDataBuffer.Post(e.Data);
}
};
//启动进程并重定向输出流
process.Start();
process.BeginOutputReadLine();
//逐行返回数据
while(等待outputDataBuffer.OutputAvailableAsync()
||等待errorDataBuffer.OutputAvailableAsync())
产生返回新的ProcessData()
{
Error=errorDataBuffer.Receive(),
Output=outputDataBuffer.Receive()
}
}
问题是,如果任何一个缓冲区在另一个方法挂起之前完成,则另一个方法挂起,因为该缓冲区的.Receive()
没有任何数据要接收。如果我将while
条件更改为和&
,则不会从其他缓冲区获取所有数据
有什么建议吗?您可以使用
ProcessData
项的单个缓冲区:
var buffer = new BufferBlock<ProcessData>();
下面是Complete
方法的实现:
bool[] completeState = new bool[2];
void Complete(int index)
{
bool completed;
lock (completeState.SyncRoot)
{
completeState[index - 1] = true;
completed = completeState.All(v => v);
}
if (completed) buffer.Complete();
}
关于实际问题,读取块的处理流程存在问题。最简单的解决方案是只使用一个缓冲区,将多个生产者和一个消费者与一个消息包结合使用 您试图用数据流块解决的概念性问题是异步流事件的基本性质。推送事件,并拉取异步流 有几种解决方案可以将它们映射到一起,尽管我认为最优雅的方法是使用无界通道作为缓冲区 通道比数据流更现代,自由度更小,比
缓冲块
更笨重,而且非常轻量级和高度优化。此外,我只需要为不同的响应类型传递一个包装器
忽略任何其他问题(概念或其他)
给定的
public enum MessageType
{
Output,
Error
}
public class Message
{
public MessageType MessageType { get; set; }
public string Data { get; set; }
public Message(string data, MessageType messageType )
{
Data = data;
MessageType = messageType;
}
}
用法
private async IAsyncEnumerable<Message> ProcessAsyncStreamAsync(
ProcessStartInfo processStartInfo,
CancellationToken cancellationToken)
{
using var process = new Process() { StartInfo = processStartInfo };
var ch = Channel.CreateUnbounded<Message>();
var completeCount = 0;
void OnReceived(string data, MessageType type)
{
// The Interlocked memory barrier is likely overkill here
if (data is null && Interlocked.Increment(ref completeCount) == 2)
ch?.Writer.Complete();
else
ch?.Writer.WriteAsync(new Message(data, type), cancellationToken);
}
process.OutputDataReceived += (_, args) => OnReceived(args.Data, MessageType.Output);
process.ErrorDataReceived += (_, args) => OnReceived(args.Data, MessageType.Error);
// start the process
// ...
await foreach (var message in ch.Reader
.ReadAllAsync(cancellationToken)
.ConfigureAwait(false))
yield return message;
// cleanup
// ...
}
专用异步IAsyncEnumerable ProcessAsyncStreamAsync(
ProcessStartInfo ProcessStartInfo,
取消令牌(取消令牌)
{
使用var process=new process(){StartInfo=processStartInfo};
var ch=Channel.CreateUnbounded();
var completeCount=0;
void OnReceived(字符串数据,消息类型)
{
//在这里,互锁的内存屏障很可能是杀伤力过大
如果(数据为空&&Interlocked.Increment(ref completeCount)==2)
ch?.Writer.Complete();
其他的
ch?.Writer.WriteAsync(新消息(数据、类型)、cancellationToken);
}
process.OutputDataReceived+=(\ux,args)=>OnReceived(args.Data,MessageType.Output);
process.ErrorDataReceived+=(\ux,args)=>OnReceived(args.Data,MessageType.Error);
//开始这个过程
// ...
等待foreach(通道读取器中的var消息
.ReadAllAsync(取消令牌)
.ConfigureWait(错误))
产生返回消息;
//清理
// ...
}
注意:完全未测试在退出时完成
void HandleData(object sender, DataReceivedEventArgs e)
{
if (e.Data != null) dataBuffer.Post(e.Data);
}
process.OutputDataReceived += HandleData;
process.ErrorDataReceived += HandleData;
process.Exited += (s,e) =>
{
process.WaitForExit();
dataBuffer.Complete();
};
是否将
Error=errorDataBuffer.OutputAvailableAsync()?errorDataBuffer.Receive():null(对于输出也是一样)对您有效吗?您在上次while
循环中检查了outputDataBuffer.OutputAvailableAsync()
两次。这是故意的还是错误?这是一个奇怪的解决方案。您是否也应该使用TryReceive
master_ruko不,使用TryReceive
方法是安全的。在多消费者场景中,如果在等待OutputAvailableAsync
后调用Receive
,则可能会获得invalidoOperationException
。另外,通过在while
循环中调用TryReceive
,您可能会在高吞吐量场景中获得更好的性能,因为OutputAvailableAsync
相对昂贵。另外,如果您对性能感兴趣,可以认为它优于BufferBlock
。他们在内部使用ValueTask
s,因此分配更少。传播struct ProcessData
而不是类也可能是有益的。虽然我确信Process
总是按照预期的顺序引发事件,但我认为没有理由期望Windows线程调度程序保证Exited
事件总是在最后一次HandleData()之后引发
已拨打电话。这些事件是在线程池中异步引发的,并且彼此不同步,因此在引发退出
事件后,总是有可能再次调用HandleData()
。换句话说,我明白为什么这种方法很诱人,但它似乎是最糟糕的选择之一(甚至不包括OP保持stdout和stderr dat
private async IAsyncEnumerable<Message> ProcessAsyncStreamAsync(
ProcessStartInfo processStartInfo,
CancellationToken cancellationToken)
{
using var process = new Process() { StartInfo = processStartInfo };
var ch = Channel.CreateUnbounded<Message>();
var completeCount = 0;
void OnReceived(string data, MessageType type)
{
// The Interlocked memory barrier is likely overkill here
if (data is null && Interlocked.Increment(ref completeCount) == 2)
ch?.Writer.Complete();
else
ch?.Writer.WriteAsync(new Message(data, type), cancellationToken);
}
process.OutputDataReceived += (_, args) => OnReceived(args.Data, MessageType.Output);
process.ErrorDataReceived += (_, args) => OnReceived(args.Data, MessageType.Error);
// start the process
// ...
await foreach (var message in ch.Reader
.ReadAllAsync(cancellationToken)
.ConfigureAwait(false))
yield return message;
// cleanup
// ...
}
void HandleData(object sender, DataReceivedEventArgs e)
{
if (e.Data != null) dataBuffer.Post(e.Data);
}
process.OutputDataReceived += HandleData;
process.ErrorDataReceived += HandleData;
process.Exited += (s,e) =>
{
process.WaitForExit();
dataBuffer.Complete();
};