J'ai essayé de résoudre cet exercice d'examen "Programmation simultanée" (en C #):
Sachant que la classe
Stream
contient les méthodesint Read(byte[] buffer, int offset, int size)
etvoid Write(byte[] buffer, int offset, int size)
, implémentez en C # la méthodeNetToFile
qui copie toutes les données reçues deNetworkStream net
à l'instanceFileStream 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 lecturenet
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".
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);
}
}
}
}
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é.
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:
Serial
est clairement le plus rapide et le plus gourmand en ressourcesVoici 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.
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!
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);
}
}
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.
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.