.NET асинхронный поток чтения / записи

я пытался решить это экзаменационное упражнение" параллельное программирование " (на C#):

зная, что Stream классе int Read(byte[] buffer, int offset, int size) и void Write(byte[] buffer, int offset, int size) методы, реализуемые в C#NetToFile метод, который копирует все данные, полученные от NetworkStream net экземпляр FileStream file экземпляра. Для передачи используйте асинхронные чтения и синхронные записи, избегая блокировки одного потока во время операций чтения. Передача заканчивается, когда 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);
}

вопрос, который у меня есть, заключается в том, что в постановке вопроса сказано:

для передачи используйте асинхронный читает и синхронно пишет, избегая один поток, который будет заблокирован во время читать операции

я не совсем уверен, что мое решение выполняет то, что требуется в этом упражнении, потому что я использую AsyncWaitHandle.WaitOne() дождаться завершения асинхронного чтения.

С другой стороны, я действительно не понимаю, что означает "неблокирующее" решение в этом сценарии, как FileStream запись должна выполняться синхронно... и чтобы сделать это, я должен подождать, пока NetworkStream чтение завершается, чтобы продолжить FileStream написание, не так ли?

не могли бы вы, пожалуйста, помочь мне с этим?


[ EDIT 1]используя обратный звонок решение

хорошо, если я понял, что Продавцов Митчел и 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);
}

как вы, возможно, заметили, я застрял в методе обратного вызова, так как я не имейте ссылку на FileStream экземпляр, где я хочу вызвать "Write(...)" метод.

кроме того, это не потокобезопасное решение, как byte[] поле выставляется и может быть разделено между параллельными NetToFile вызовы. Я не знаю, как решить эту проблему, не подвергая это byte[] поле во внешней области... и я почти уверен, что он не может быть раскрыт таким образом.

я не хочу использовать лямбда и анонимный метод решения, потому что это не в учебной программе курса" параллельное программирование".

6 ответов


вам нужно будет использовать обратный вызов из чтения 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);
            }
        }
    }
}

даже если это идет против зерна, чтобы помочь людям с их домашней работой, учитывая, что это более года, вот правильный способ сделать это. Все, что вам нужно перекрытие ваши операции чтения / записи - не требуется создание дополнительных потоков или что-либо еще.

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), но я думаю, что это пылесос подход ко всей асинхронной копии.

мне нужно CopyTransformAsync где я мог передать делегата, чтобы сделать что-то, поскольку куски были переданы через операцию копирования. например, вычислить дайджест сообщения при копировании. Вот почему я заинтересовался своим собственным. выбор.

выводы:

  • CopyToAsync bufferSize чувствителен (требуется большой буфер)
  • FileOptions.Асинхронный -> делает его ужасно медленным (не уверен, почему это так)
  • размер буфера объектов FileStream может быть меньше (это не так важно)
  • на 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

здесь вы можете увидеть Process Explorer, график производительности при запуске теста. В основном каждый top (в нижний из трех графиков) является началом серийного теста. Вы можете ясно видеть, как резко увеличивается пропускная способность по мере увеличения размера буфера. Казалось бы, он планирует где-то около 80K, что является то, что .NET framework CopyToAsync метод использует, внутренне.

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
}

этот метод чередования чтения/записи, несмотря на огромных буферов где-то между 18% быстрее, чем BCL CopyToAsync.

из любопытства я изменил асинхронные вызовы на типичные вызовы begin / end async pattern, и это не улучшило ситуацию ни на йоту, это сделало ее хуже. Для всех, кого я люблю колотить по абстракции задач, они делают некоторые изящные вещи, когда вы пишете код с ключевыми словами 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 team (Stephen Toub) о том, как реализовать параллельную копию асинхронного потока. Пост содержит устаревшие справочную образцам так вот допущены один:
Get дополнительные параллельные расширения из кода.в MSDN затем

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

также рассмотреть возможность использования Ж. богаче по AsyncEnumerator.


вы правы, то, что вы делаете, в основном Синхронное чтение, потому что вы используете метод WaitOne (), и он просто останавливает выполнение, пока данные не будут готовы, это в основном то же самое, что делать это с помощью Read() вместо BeginRead() и EndRead().

Что вам нужно сделать, это использовать аргумент обратного вызова в методе BeginRead (), с ним вы определяете метод обратного вызова (или лямбда-выражение), этот метод будет вызываться, когда информация была прочитана (в обратном вызове метод, который вы должны проверить на конец потока и записать в выходной поток), таким образом, вы не будете блокировать основной поток (Вам не понадобится WaitOne () или EndRead ().

надеюсь, что это помогает.