私はこの「並行プログラミング」試験問題を解決しようとしています(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[]
_フィールドを外部スコープで公開せずにこの問題を解決する方法はわかりません...そして、この方法で公開されない可能性がほぼ確実です。
ラムダまたは匿名メソッドソリューションを使用したくないのは、それが「同時プログラミング」コースのカリキュラムに含まれていないためです。
これを処理するには、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);
}
}
}
}
宿題を手伝うことは穀物に反しますが、これが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;
}
}
乾杯。
これは最速のコード(.NETタスクの抽象化によるオーバーヘッドがある)であるとは思いませんが、非同期コピー全体に対するcleanerアプローチだと思います。
コピー操作でチャンクが渡されるときに、デリゲートを渡して何かを実行できるCopyTransformAsync
が必要でした。例えばコピー中にメッセージダイジェストを計算します。それが、私が自分の選択肢を展開することに興味を持った理由です。
結果:
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前後のどこかを計画しているように見えます。
ここでの良いところは、最終的な実装がそれほど複雑ではなかったことです。
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キーワードを使用してコードを記述すると、彼らは気の利いたことをします。そのコードを読む方がはるかに便利です。
うわー、これらはすべて非常に複雑です!ここに私の非同期ソリューションがあり、それはただ一つの機能です。 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);
}
}
誰も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 の使用を検討してください。
正しいのは、基本的に同期読み取りです。WaitOne()メソッドを使用し、データの準備ができるまで実行を停止するだけです。これは基本的に、BeginRead( )およびEndRead()。
あなたがしなければならないことは、BeginRead()メソッドでコールバック引数を使用することです、それで、コールバックメソッド(またはラムダ式)を定義し、このメソッドは情報が読み込まれたときに呼び出されます(コールバックメソッドでストリームの終わりを確認し、出力ストリームに書き込む必要があります)、この方法ではメインスレッドをブロックしません(WaitOne()もEndRead()も必要ありません)。
お役に立てれば。