.net 流多播-读取一个流一次,但以不同的方式处理它,以最小的缓冲

.net 流多播-读取一个流一次,但以不同的方式处理它,以最小的缓冲,.net,asynchronous,stream,task-parallel-library,system.reactive,.net,Asynchronous,Stream,Task Parallel Library,System.reactive,为了可伸缩性和节省资源,最好避免将整个输入流读取到内存中,而是尝试将其作为流处理,一次读取小块。在.NET中,当您有一件事情要处理数据时,这很容易实现,比如从web请求中读取数据并将其保存到文件中。简单的例子: input.CopyTo(output); // reads chunks of 4096 bytes and writes them to `output` 但当我想用这些数据做多种事情时,这就有点棘手了。例如,我想: 计算不支持length属性的流的长度。我们可以使用自定义的Du

为了可伸缩性和节省资源,最好避免将整个输入流读取到内存中,而是尝试将其作为流处理,一次读取小块。在.NET中,当您有一件事情要处理数据时,这很容易实现,比如从web请求中读取数据并将其保存到文件中。简单的例子:

input.CopyTo(output); // reads chunks of 4096 bytes and writes them to `output`
但当我想用这些数据做多种事情时,这就有点棘手了。例如,我想:

  • 计算不支持
    length
    属性的流的长度。我们可以使用自定义的
    DummyStream
    来实现这一点,它除了跟踪长度外,对写入其中的数据不做任何处理
  • 找出数据的Mime类型。只需要流的前256个字节
  • 拯救整个世界
。。。但是,只需在输入流上进行一次传递,而缓冲的使用最少

我相信这是可能的。我可能可以协调多个线程,其中一个线程实际读取输入流,其他线程执行我想要执行的每个“处理”任务。但如果做得不对,这很容易变得相当复杂和脆弱

我的问题是:

  • 我必须使用多个线程吗?我会喜欢某种共同例行的解决方案,其中所有的处理是交织在一个线程
  • 我有没有办法利用C#的
    async
    特性或反应式扩展来简化这个解决方案
我的大脑在这件事上有麻烦。我正在寻找实现这一目标的最佳(干净、可维护、高效利用计算机资源)方法的指导,特别是在TPL、
async
和RX等新技术的背景下


这是我设想的语法示例:

public static void Multicast(this Stream input, params Action<Stream>[] processingActions)
{
    // TODO: ??? complicated stream multicasting logic goes here. ???
    throw new NotImplementedException();
}
下面是一个处理操作的示例:

private static byte[] CalculateMd5(Stream input)
{
    return MD5.Create().ComputeHash(input);
}

使用Rx,您可以使用
Observable。创建
来创建一个可读取流的Observable,然后使用
Publish
来允许对steam进行多个订阅,而不启动它,然后在
Published
流上调用
Connect
,以启动并运行所有内容。您可以使用
ObserveOn
SubscribeOn
为流数据的每个不同“路由”确定代码的每个部分何时、何地以及如何运行,这意味着您可以缓冲整个流并一次性将其提交到数据库,对MD5也可以这样做,使用
Scan
Aggregate
对流进行计数,但也可以有一个“路由”来确定mime类型并提前取消订阅。另外,如果需要将这些元素重新同步在一起,可以使用
combinelateest

这个问题对我来说非常有趣,我希望现在有时间发布一些真实的代码示例。不幸的是,我没有。希望这能让您了解在什么配置中可以使用什么操作符来完成您所寻找的任务

下面是一些非流读取部分的psuedo代码

var connectable = ReadStreamAsObservable(stream).Publish();

var mimeType = connectable.ReadMimeTypeAsObservable();
var md5      = connectable.ReadMD5AsObservable();
var record   = connectable.SubmitToDatabaseAsObservable(myDbConnection);
var length   = connectable.Aggregate(0, (acc, x) => acc + x.Bytes.Length);

var parts = Observable.CombineLatest(mimeType, md5, length, record,
  (mimeType, md5, length, record) => new {
    MimeType = mimeType,
    MD5 = md5,
    Length = length,
    Record = record
  });

var subscription = new CompositeDisposable(
  parts.Subscribe((x) => Console.WriteLine(x)),
  connectable.Connect()
  );

我认为您想要做的是有一个输入流,多个输出流,然后将输入复制到所有输出,类似于:

Stream input;
IList<Stream> outputs;

byte[] buffer = new byte[BufferSize];
int read;

while ((read = input.Read(buffer, 0, buffer.Length)) != 0)
{
    foreach (var output in outputs)
    {
        output.Write(buffer, 0, read);
    }
}

这个流可以直接用作计数流,也可以轻松实现find MIME流。

我决定尝试一下Rx实现。这是我到目前为止得到的。它不进行任何数据库写入,但只需对文件进行一次遍历并使用最小缓冲,即可计算长度、MD5哈希和mimetype

使用系统;
使用系统诊断;
使用System.IO;
使用系统反应性一次性用品;
使用System.Reactive.Linq;
使用System.Runtime.InteropServices;
使用System.Security.Cryptography;
名称空间RxTest
{
内部静态类程序
{
私有静态void Main()
{
var expectedValues=readexpectedvalues直接(“demo.txt”);
新文件信息(“demo.txt”)
.ReadasObservable(4096)
.ToFileData()
.Subscribe(观察值=>比较(期望值,观察值));
}
私有静态void比较(需要FileData,观察到FileData)
{
Console.WriteLine();
WriteLine(“预期的”,预期的);
书写线(“观察到的”,观察到的);
Console.WriteLine();
Assert(observed.Length==expected.Length);
Assert(BitConverter.ToString(observed.Hash)=BitConverter.ToString(expected.Hash));
Assert(observed.MimeType==expected.MimeType);
}
私有静态void WriteLine(字符串前缀,观察到的文件数据)
{
Console.WriteLine(“{0}:{1:N0}{2}{3}”,
前缀
注意,长度,
观察到.MimeType,
ToString(observed.Hash).Replace(“-”,”);
}
私有静态文件数据直接读取ExpectedValues(字符串文件名)
{
返回新文件数据
{
长度=新文件信息(文件名).Length,
Hash=MD5.Create().ComputeHash(File.ReadAllBytes(fileName)),
MimeType=FileDataExtensions.FindMimeType(GetFirst256Bytes(fileName))
};
}
私有静态字节[]GetFirst256Bytes(字符串路径)
{
使用(var stream=File.OpenRead(path))
{
var buffer=新字节[256];
如果(stream.Length>=256)
读取(缓冲区,0,256);
其他的
读取(缓冲区,0,(int)stream.Length);
返回缓冲区;
}
}
}
公共类文件数据
{
公共长长度{get;set;}
公共字符串M
Stream input;
IList<Stream> outputs;

byte[] buffer = new byte[BufferSize];
int read;

while ((read = input.Read(buffer, 0, buffer.Length)) != 0)
{
    foreach (var output in outputs)
    {
        output.Write(buffer, 0, read);
    }
}
public class OutputStreamBase : Stream
{
    private int length;

    public override void Flush()
    {}

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotSupportedException();
    }

    public override void SetLength(long value)
    {
        throw new NotSupportedException();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotSupportedException();
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        length += count;
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanSeek
    {
        get { return false; }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override long Length
    {
        get { return length; }
    }

    public override long Position
    {
        get { return length; }
        set { throw new NotSupportedException(); }
    }
}