web-dev-qa-db-fra.com

Lecture / écriture de flux asynchrone .NET

J'ai essayé de résoudre cet exercice d'examen "Programmation simultanée" (en C #):

Sachant que la classe Stream contient les méthodes int Read(byte[] buffer, int offset, int size) et void Write(byte[] buffer, int offset, int size), implémentez en C # la méthode NetToFile qui copie toutes les données reçues de NetworkStream net à l'instance FileStream file. Pour effectuer le transfert, utilisez des lectures asynchrones et des écritures synchrones, en évitant qu'un thread ne soit bloqué pendant les opérations de lecture. Le transfert se termine lorsque l'opération de lecture net renvoie la valeur 0. Pour simplifier, il n'est pas nécessaire de prendre en charge l'annulation contrôlée de l'opération.

void NetToFile(NetworkStream net, FileStream file);

J'ai essayé de résoudre cet exercice, mais je me bats avec une question liée à la question elle-même. Mais d'abord, voici mon code:

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);
}

Ma question est que, dans la déclaration de question, il est dit:

Pour effectuer le transfert, utilisez des lectures asynchrones et des écritures synchrones, en évitant qu'un thread ne soit bloqué pendant les opérations de lecture

Je ne suis pas vraiment sûr que ma solution accomplisse ce que l'on souhaite dans cet exercice, car j'utilise AsyncWaitHandle.WaitOne() pour attendre la fin de la lecture asynchrone.

D'un autre côté, je ne sais pas vraiment ce qui est censé être une solution "non bloquante" dans ce scénario, car l'écriture FileStream est censée être faite de manière synchrone ... et pour ce faire , Je dois attendre la fin de la lecture de NetworkStream pour procéder à l'écriture de FileStream, n'est-ce pas?

Pouvez-vous, s'il vous plaît, m'aider avec ça?


[EDIT 1] Utilisation de callback solution

Ok, si j'ai compris ce que Mitchel Sellers et willvv ont répondu, on m'a conseillé d'utiliser une méthode de rappel pour en faire une solution "non bloquante". Voici donc mon code:

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);
}

Comme vous l'avez peut-être remarqué, je suis bloqué sur la méthode de rappel, car je n'ai pas de référence à l'instance FileStream où je veux invoquer la méthode "Write (...)".

De plus, ce n'est pas une solution thread-safe, car le champ byte[] Est exposé et peut être partagé entre des appels simultanés NetToFile. Je ne sais pas comment résoudre ce problème sans exposer ce champ byte[] Dans la portée externe ... et je suis presque sûr qu'il ne peut pas être exposé de cette façon.

Je ne veux pas utiliser une solution lambda ou une méthode anonyme, car cela ne fait pas partie du programme de cours "Programmation simultanée".

46
XpiritO

Vous devrez utiliser le rappel de la lecture de NetStream pour gérer cela. Et franchement, il pourrait être plus facile d'encapsuler la logique de copie dans sa propre classe afin de pouvoir conserver l'instance des Streams actifs.

Voici comment je l'aborderais (non testé):

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

Même si cela va à contre-courant d'aider les gens à faire leurs devoirs, étant donné qu'il s'agit de plus d'un an, voici la bonne façon d'y parvenir. Tout ce dont vous avez besoin pour chevauchement vos opérations de lecture/écriture - aucune génération de threads supplémentaires ou autre n'est nécessaire.

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;
    }

}

À votre santé.

52
Nicholas Carey

Je doute que ce soit le code le plus rapide (il y a une surcharge de l'abstraction de la tâche .NET) mais je pense que c'est une approche plus propre pour toute la copie asynchrone.

J'avais besoin d'un CopyTransformAsync où je pourrais passer un délégué pour faire quelque chose pendant que des morceaux étaient passés par l'opération de copie. par exemple. calculer un résumé de message lors de la copie. C'est pourquoi je me suis intéressé à rouler ma propre option.

Constatations:

  • CopyToAsync bufferSize est sensible (un grand tampon est requis)
  • FileOptions.Asynchronous -> le rend horriblement lent (je ne sais pas exactement pourquoi)
  • Le bufferSize des objets FileStream peut être plus petit (ce n'est pas si important)
  • Le test Serial est clairement le plus rapide et le plus gourmand en ressources

Voici ce que j'ai trouvé et le code source complet pour le programme que j'ai utilisé pour tester cela. Sur ma machine, ces tests ont été exécutés sur un disque SSD et équivalent à une copie de fichier. Normalement, vous ne voudriez pas l'utiliser pour copier uniquement des fichiers, mais lorsque vous avez un flux réseau (ce qui est mon cas d'utilisation), c'est à ce moment que vous souhaitez utiliser quelque chose comme ça.

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

Ici vous pouvez voir l'Explorateur de processus, graphique des performances pendant l'exécution du test. Fondamentalement, chaque en haut (dans le bas des trois graphiques) est le début du test en série. Vous pouvez clairement voir comment le débit augmente considérablement à mesure que la taille du tampon augmente. Il semblerait qu'elle planifie quelque part autour de 80K, ce que la méthode .NET Framework CopyToAsync utilise en interne.

Performance Graph

La bonne chose ici est que la mise en œuvre finale n'était pas si compliquée:

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
}

Cette méthode d'entrelacement de la lecture/écriture malgré les énormes tampons est quelque part entre 18% plus rapide que le BCL CopyToAsync.

Par curiosité, j'ai changé les appels asynchrones en appels de modèle asynchrone de début/fin typiques et cela n'a pas amélioré la situation du tout, cela a empiré. Pour tout ce que j'aime bash sur la surcharge d'abstraction de tâche, ils font des choses astucieuses lorsque vous écrivez votre code avec les mots-clés async/wait et il est beaucoup plus agréable de lire ce code!

16
John Leidegren

Wow, ce sont tous très complexes! Voici ma solution asynchrone, et ce n'est qu'une fonction. Read () et BeginWrite () s'exécutent tous les deux en même temps.

/// <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

C'est étrange que personne n'ait mentionné TPL.
Ici Le très bon article de l'équipe PFX (Stephen Toub) sur la façon d'implémenter la copie simultanée de flux asynchrone. Le message contient des références obsolètes aux échantillons, alors voici un correct:
Obtenez Extensions d'extensions parallèles à partir de code.msdn puis

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

Pensez également à utiliser AsyncEnumerator de J.Richer.

9
Shrike

Vous avez raison, ce que vous faites est essentiellement une lecture synchrone, car vous utilisez la méthode WaitOne () et elle arrête simplement l'exécution jusqu'à ce que les données soient prêtes, c'est essentiellement la même chose que de le faire en utilisant Read () au lieu de BeginRead ( ) et EndRead ().

Ce que vous devez faire, c'est utiliser l'argument de rappel dans la méthode BeginRead (), avec lui, vous définissez une méthode de rappel (ou une expression lambda), cette méthode sera invoquée lorsque les informations auront été lues (dans la méthode de rappel que vous devez vérifier la fin du flux et écrire dans le flux de sortie), de cette façon, vous ne bloquerez pas le thread principal (vous n'aurez pas besoin de WaitOne () ni de EndRead ().

J'espère que cela t'aides.

0
willvv