web-dev-qa-db-ja.com

.NET非同期ストリームの読み取り/書き込み

私はこの「並行プログラミング」試験問題を解決しようとしています(C#):

Streamクラスにint Read(byte[] buffer, int offset, int size)およびvoid Write(byte[] buffer, int offset, int size)メソッドが含まれていることを知って、NetworkStream net_インスタンスから受信したすべてのデータをコピーするNetToFileメソッドをC#で実装します。 _FileStream file_インスタンスに。転送を行うには、非同期読み取りと同期書き込みを使用して、読み取り操作中に1つのスレッドがブロックされないようにします。 net読み取り操作が値0を返すと、転送は終了します。簡単にするために、操作のキャンセルを制御する必要はありません。

_void NetToFile(NetworkStream net, FileStream file);
_

私はこの演習を解決しようとしてきましたが、質問自体に関連する質問に苦労しています。しかし、最初に、ここに私のコードがあります:

_public static void NetToFile(NetworkStream net, FileStream file) {
    byte[] buffer = new byte[4096]; // buffer with 4 kB dimension
    int offset = 0; // read/write offset
    int nBytesRead = 0; // number of bytes read on each cycle

    IAsyncResult ar;
    do {
        // read partial content of net (asynchronously)
        ar = net.BeginRead(buffer,offset,buffer.Length,null,null);
        // wait until read is completed
        ar.AsyncWaitHandle.WaitOne();
        // get number of bytes read on each cycle
        nBytesRead = net.EndRead(ar);

        // write partial content to file (synchronously)
        fs.Write(buffer,offset,nBytesRead);
        // update offset
        offset += nBytesRead;
    }
    while( nBytesRead > 0);
}
_

私の質問は、質問文で次のように言われているということです。

転送を行うには、非同期読み取りと同期書き込みを使用して、読み取り操作中に1つのスレッドがブロックされないようにします

非同期読み取りが完了するまで待機するためにAsyncWaitHandle.WaitOne()を使用しているため、私のソリューションがこの演習で必要なことを達成できるかどうかはわかりません。

反対に、FileStreamの書き込みは同期的に行われることを意図しているため、このシナリオで「非ブロッキング」ソリューションとなるものを本当に理解していません... 、NetworkStreamの読み取りが完了するまで待ってからFileStreamの書き込みを続行する必要がありますか?

これを手伝ってくれませんか?


[編集1]callbacksolutionを使用

わかりました。 Mitchel Sellers および willvv の回答を理解した場合、コールバックメソッドを使用してこれを「非ブロッキング」ソリューションに変えるように勧められました。ここに私のコードがあります:

_byte[] buffer; // buffer

public static void NetToFile(NetworkStream net, FileStream file) {
    // buffer with same dimension as file stream data
    buffer = new byte[file.Length];
    //start asynchronous read
    net.BeginRead(buffer,0,buffer.Length,OnEndRead,net);
}

//asynchronous callback
static void OnEndRead(IAsyncResult ar) {
    //NetworkStream retrieve
    NetworkStream net = (NetworkStream) ar.IAsyncState;
    //get number of bytes read
    int nBytesRead = net.EndRead(ar);

    //write content to file
    //... and now, how do I write to FileStream instance without
    //having its reference??
    //fs.Write(buffer,0,nBytesRead);
}
_

お気づきかもしれませんが、「Write(...)」メソッドを呼び出すFileStreamインスタンスへの参照がないため、コールバックメソッドにこだわっています。

さらに、_byte[]_フィールドが公開されており、同時NetToFile呼び出し間で共有される可能性があるため、これはスレッドセーフなソリューションではありません。この_byte[]_フィールドを外部スコープで公開せずにこの問題を解決する方法はわかりません...そして、この方法で公開されない可能性がほぼ確実です。

ラムダまたは匿名メソッドソリューションを使用したくないのは、それが「同時プログラミング」コースのカリキュラムに含まれていないためです。

46
XpiritO

これを処理するには、NetStream読み取りからのコールバックを使用する必要があります。そして率直に言って、アクティブなストリームのインスタンスを維持できるように、コピーロジックを独自のクラスにラップする方が簡単かもしれません。

これは私がそれにアプローチする方法です(テストされていません):

public class Assignment1
{
    public static void NetToFile(NetworkStream net, FileStream file) 
    {
        var copier = new AsyncStreamCopier(net, file);
        copier.Start();
    }

    public static void NetToFile_Option2(NetworkStream net, FileStream file) 
    {
        var completedEvent = new ManualResetEvent(false);

        // copy as usual but listen for completion
        var copier = new AsyncStreamCopier(net, file);
        copier.Completed += (s, e) => completedEvent.Set();
        copier.Start();

        completedEvent.WaitOne();
    }

    /// <summary>
    /// The Async Copier class reads the input Stream Async and writes Synchronously
    /// </summary>
    public class AsyncStreamCopier
    {
        public event EventHandler Completed;

        private readonly Stream input;
        private readonly Stream output;

        private byte[] buffer = new byte[4096];

        public AsyncStreamCopier(Stream input, Stream output)
        {
            this.input = input;
            this.output = output;
        }

        public void Start()
        {
            GetNextChunk();
        }

        private void GetNextChunk()
        {
            input.BeginRead(buffer, 0, buffer.Length, InputReadComplete, null);
        }

        private void InputReadComplete(IAsyncResult ar)
        {
            // input read asynchronously completed
            int bytesRead = input.EndRead(ar);

            if (bytesRead == 0)
            {
                RaiseCompleted();
                return;
            }

            // write synchronously
            output.Write(buffer, 0, bytesRead);

            // get next
            GetNextChunk();
        }

        private void RaiseCompleted()
        {
            if (Completed != null)
            {
                Completed(this, EventArgs.Empty);
            }
        }
    }
}
12
bendewey

宿題を手伝うことは穀物に反しますが、これが1年以上前であることを考えると、これを達成する適切な方法があります。必要なのはoverlap読み取り/書き込み操作です。追加のスレッドの生成などは必要ありません。

public static class StreamExtensions
{
    private const int DEFAULT_BUFFER_SIZE = short.MaxValue ; // +32767
    public static void CopyTo( this Stream input , Stream output )
    {
        input.CopyTo( output , DEFAULT_BUFFER_SIZE ) ;
        return ;
    }
    public static void CopyTo( this Stream input , Stream output , int bufferSize )
    {
        if ( !input.CanRead ) throw new InvalidOperationException(   "input must be open for reading"  );
        if ( !output.CanWrite ) throw new InvalidOperationException( "output must be open for writing" );

        byte[][]     buf   = { new byte[bufferSize] , new byte[bufferSize] } ;
        int[]        bufl  = { 0 , 0 }                                       ;
        int          bufno = 0 ;
        IAsyncResult read  = input.BeginRead( buf[bufno] , 0 , buf[bufno].Length , null , null ) ;
        IAsyncResult write = null ;

        while ( true )
        {

            // wait for the read operation to complete
            read.AsyncWaitHandle.WaitOne() ; 
            bufl[bufno] = input.EndRead(read) ;

            // if zero bytes read, the copy is complete
            if ( bufl[bufno] == 0 )
            {
                break ;
            }

            // wait for the in-flight write operation, if one exists, to complete
            // the only time one won't exist is after the very first read operation completes
            if ( write != null )
            {
                write.AsyncWaitHandle.WaitOne() ;
                output.EndWrite(write) ;
            }

            // start the new write operation
            write = output.BeginWrite( buf[bufno] , 0 , bufl[bufno] , null , null ) ;

            // toggle the current, in-use buffer
            // and start the read operation on the new buffer.
            //
            // Changed to use XOR to toggle between 0 and 1.
            // A little speedier than using a ternary expression.
            bufno ^= 1 ; // bufno = ( bufno == 0 ? 1 : 0 ) ;
            read = input.BeginRead( buf[bufno] , 0 , buf[bufno].Length , null , null ) ;

        }

        // wait for the final in-flight write operation, if one exists, to complete
        // the only time one won't exist is if the input stream is empty.
        if ( write != null )
        {
            write.AsyncWaitHandle.WaitOne() ;
            output.EndWrite(write) ;
        }

        output.Flush() ;

        // return to the caller ;
        return ;
    }


    public static async Task CopyToAsync( this Stream input , Stream output )
    {
        await input.CopyToAsync( output , DEFAULT_BUFFER_SIZE ) ;
        return;
    }

    public static async Task CopyToAsync( this Stream input , Stream output , int bufferSize )
    {
        if ( !input.CanRead ) throw new InvalidOperationException( "input must be open for reading" );
        if ( !output.CanWrite ) throw new InvalidOperationException( "output must be open for writing" );

        byte[][]     buf   = { new byte[bufferSize] , new byte[bufferSize] } ;
        int[]        bufl  = { 0 , 0 } ;
        int          bufno = 0 ;
        Task<int>    read  = input.ReadAsync( buf[bufno] , 0 , buf[bufno].Length ) ;
        Task         write = null ;

        while ( true )
        {

            await read ;
            bufl[bufno] = read.Result ;

            // if zero bytes read, the copy is complete
            if ( bufl[bufno] == 0 )
            {
                break;
            }

            // wait for the in-flight write operation, if one exists, to complete
            // the only time one won't exist is after the very first read operation completes
            if ( write != null )
            {
                await write ;
            }

            // start the new write operation
            write = output.WriteAsync( buf[bufno] , 0 , bufl[bufno] ) ;

            // toggle the current, in-use buffer
            // and start the read operation on the new buffer.
            //
            // Changed to use XOR to toggle between 0 and 1.
            // A little speedier than using a ternary expression.
            bufno ^= 1; // bufno = ( bufno == 0 ? 1 : 0 ) ;
            read = input.ReadAsync( buf[bufno] , 0 , buf[bufno].Length );

        }

        // wait for the final in-flight write operation, if one exists, to complete
        // the only time one won't exist is if the input stream is empty.
        if ( write != null )
        {
            await write;
        }

        output.Flush();

        // return to the caller ;
        return;
    }

}

乾杯。

52
Nicholas Carey

これは最速のコード(.NETタスクの抽象化によるオーバーヘッドがある)であるとは思いませんが、非同期コピー全体に対するcleanerアプローチだと思います。

コピー操作でチャンクが渡されるときに、デリゲートを渡して何かを実行できるCopyTransformAsyncが必要でした。例えばコピー中にメッセージダイジェストを計算します。それが、私が自分の選択肢を展開することに興味を持った理由です。

結果:

  • CopyToAsync bufferSizeは敏感です(大きなバッファーが必要です)
  • FileOptions.Asynchronous->恐ろしく遅くなります(正確な理由はわかりません)
  • FileStreamオブジェクトのbufferSizeは小さくすることができます(それほど重要ではありません)
  • Serialテストは明らかに最速で最もリソースを消費します

ここに私が見つけたものと 完全なソースコード これをテストするために使用したプログラムの私のマシンでは、これらのテストはSSDディスクで実行され、ファイルコピーと同等です。通常、ファイルをコピーするためだけにこれを使用するのではなく、代わりにネットワークストリームがある場合(これは私のユースケースです)、このようなものを使用する場合です。

4K buffer

Serial...                                in 0.474s
CopyToAsync...                           timed out
CopyToAsync (Asynchronous)...            timed out
CopyTransformAsync...                    timed out
CopyTransformAsync (Asynchronous)...     timed out

8K buffer

Serial...                                in 0.344s
CopyToAsync...                           timed out
CopyToAsync (Asynchronous)...            timed out
CopyTransformAsync...                    in 1.116s
CopyTransformAsync (Asynchronous)...     timed out

40K buffer

Serial...                                in 0.195s
CopyToAsync...                           in 0.624s
CopyToAsync (Asynchronous)...            timed out
CopyTransformAsync...                    in 0.378s
CopyTransformAsync (Asynchronous)...     timed out

80K buffer

Serial...                                in 0.190s
CopyToAsync...                           in 0.355s
CopyToAsync (Asynchronous)...            in 1.196s
CopyTransformAsync...                    in 0.300s
CopyTransformAsync (Asynchronous)...     in 0.886s

160K buffer

Serial...                                in 0.432s
CopyToAsync...                           in 0.252s
CopyToAsync (Asynchronous)...            in 0.454s
CopyTransformAsync...                    in 0.447s
CopyTransformAsync (Asynchronous)...     in 0.555s

ここでは、プロセスエクスプローラー、テストの実行時のパフォーマンスグラフを見ることができます。基本的に、各top(3つのグラフの下部)は、シリアルテストの開始です。バッファサイズが大きくなるにつれてスループットが劇的に増加する様子を明確に見ることができます。それは、.NETフレームワークのCopyToAsyncメソッドが内部的に使用する80K前後のどこかを計画しているように見えます。

Performance Graph

ここでの良いところは、最終的な実装がそれほど複雑ではなかったことです。

static Task CompletedTask = ((Task)Task.FromResult(0));
static async Task CopyTransformAsync(Stream inputStream
    , Stream outputStream
    , Func<ArraySegment<byte>, ArraySegment<byte>> transform = null
    )
{
    var temp = new byte[bufferSize];
    var temp2 = new byte[bufferSize];

    int i = 0;

    var readTask = inputStream
        .ReadAsync(temp, 0, bufferSize)
        .ConfigureAwait(false);

    var writeTask = CompletedTask.ConfigureAwait(false);

    for (; ; )
    {
        // synchronize read
        int read = await readTask;
        if (read == 0)
        {
            break;
        }

        if (i++ > 0)
        {
            // synchronize write
            await writeTask;
        }

        var chunk = new ArraySegment<byte>(temp, 0, read);

        // do transform (if any)
        if (!(transform == null))
        {
            chunk = transform(chunk);
        }

        // queue write
        writeTask = outputStream
            .WriteAsync(chunk.Array, chunk.Offset, chunk.Count)
            .ConfigureAwait(false);

        // queue read
        readTask = inputStream
            .ReadAsync(temp2, 0, bufferSize)
            .ConfigureAwait(false);

        // swap buffer
        var temp3 = temp;
        temp = temp2;
        temp2 = temp3;
    }

    await writeTask; // complete any lingering write task
}

巨大なバッファにもかかわらず読み取り/書き込みをインターリーブするこの方法は、BCL CopyToAsyncよりも18%高速です。

好奇心から、非同期呼び出しを通常の開始/終了非同期パターン呼び出しに変更しましたが、状況は少し改善されず、悪化しました。タスク抽象化のオーバーヘッドを大嫌いするすべての人にとって、async/awaitキーワードを使用してコードを記述すると、彼らは気の利いたことをします。そのコードを読む方がはるかに便利です。

16
John Leidegren

うわー、これらはすべて非常に複雑です!ここに私の非同期ソリューションがあり、それはただ一つの機能です。 Read()とBeginWrite()は両方とも同時に実行されます。

/// <summary>
/// Copies a stream.
/// </summary>
/// <param name="source">The stream containing the source data.</param>
/// <param name="target">The stream that will receive the source data.</param>
/// <remarks>
/// This function copies until no more can be read from the stream
///  and does not close the stream when done.<br/>
/// Read and write are performed simultaneously to improve throughput.<br/>
/// If no data can be read for 60 seconds, the copy will time-out.
/// </remarks>
public static void CopyStream(Stream source, Stream target)
{
    // This stream copy supports a source-read happening at the same time
    // as target-write.  A simpler implementation would be to use just
    // Write() instead of BeginWrite(), at the cost of speed.

    byte[] readbuffer = new byte[4096];
    byte[] writebuffer = new byte[4096];
    IAsyncResult asyncResult = null;

    for (; ; )
    {
        // Read data into the readbuffer.  The previous call to BeginWrite, if any,
        //  is executing in the background..
        int read = source.Read(readbuffer, 0, readbuffer.Length);

        // Ok, we have read some data and we're ready to write it, so wait here
        //  to make sure that the previous write is done before we write again.
        if (asyncResult != null)
        {
            // This should work down to ~0.01kb/sec
            asyncResult.AsyncWaitHandle.WaitOne(60000);
            target.EndWrite(asyncResult); // Last step to the 'write'.
            if (!asyncResult.IsCompleted) // Make sure the write really completed.
                throw new IOException("Stream write failed.");
        }

        if (read <= 0)
            return; // source stream says we're done - nothing else to read.

        // Swap the read and write buffers so we can write what we read, and we can
        //  use the then use the other buffer for our next read.
        byte[] tbuf = writebuffer;
        writebuffer = readbuffer;
        readbuffer = tbuf;

        // Asynchronously write the data, asyncResult.AsyncWaitHandle will
        // be set when done.
        asyncResult = target.BeginWrite(writebuffer, 0, read, null, null);
    }
}
11
Kenzi

誰もTPLに言及していないのは奇妙です。
ここ は、同時非同期ストリームコピーを実装する方法に関するPFXチーム(Stephen Toub)による非常に素晴らしい投稿です。投稿にはサンプルの期限切れの保護が含まれているため、ここに最新のものがあります:
Get code.msdnのParallel Extensions Extras then

var task = sourceStream.CopyStreamToStreamAsync(destinationStream);
// do what you want with the task, for example wait when it finishes:
task.Wait();

また、J.Richerの AsyncEnumerator の使用を検討してください。

9
Shrike

正しいのは、基本的に同期読み取りです。WaitOne()メソッドを使用し、データの準備ができるまで実行を停止するだけです。これは基本的に、BeginRead( )およびEndRead()。

あなたがしなければならないことは、BeginRead()メソッドでコールバック引数を使用することです、それで、コールバックメソッド(またはラムダ式)を定義し、このメソッドは情報が読み込まれたときに呼び出されます(コールバックメソッドでストリームの終わりを確認し、出力ストリームに書き込む必要があります)、この方法ではメインスレッドをブロックしません(WaitOne()もEndRead()も必要ありません)。

お役に立てれば。

0
willvv