2014-05-07 13 views
14

Scrittura di String Builder su file in modo asincrono. Questo codice prende il controllo di un file, scrive un flusso su di esso e lo rilascia. Tratta le richieste da operazioni asincrone, che possono entrare in qualsiasi momento.Scrittura su file in modo thread safe

FilePath è impostato per istanze di classe (quindi l'oggetto di blocco è per istanza), ma esiste il rischio di conflitto poiché tali classi potrebbero condividere FilePath. Questo tipo di conflitto, così come tutti gli altri tipi al di fuori dell'istanza della classe, verrebbero affrontati con tentativi.

Questo codice è adatto allo scopo? C'è un modo migliore per gestire ciò che significa meno (o nessuna) dipendenza dal meccanismo di cattura e riprova?

Inoltre, come evitare di rilevare eccezioni che si sono verificate per altri motivi.

public string Filepath { get; set; } 
private Object locker = new Object(); 

public async Task WriteToFile(StringBuilder text) 
    { 
     int timeOut = 100; 
     Stopwatch stopwatch = new Stopwatch(); 
     stopwatch.Start(); 
     while (true) 
     { 
      try 
      { 
       //Wait for resource to be free 
       lock (locker) 
       { 
        using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read)) 
        using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode)) 
        { 
         writer.Write(text.ToString()); 
        } 
       } 
       break; 
      } 
      catch 
      { 
       //File not available, conflict with other class instances or application 
      } 
      if (stopwatch.ElapsedMilliseconds > timeOut) 
      { 
       //Give up. 
       break; 
      } 
      //Wait and Retry 
      await Task.Delay(5); 
     } 
     stopwatch.Stop(); 
    } 
+0

Le scritture di file effettive non sono asincrone. Intendevi che fossero? –

+0

@StephenCleary, sì, c'erano inizialmente ma non dovresti aspettare le cose in un lucchetto, quindi ho dovuto cambiarlo. Non sono sicuro di quanto mina l'intera faccenda. –

+0

Sembra che dovrebbe essere su codereview tbh –

risposta

27

Il modo in cui ci si avvicina dipende molto dalla frequenza con cui si scrive. Se stai scrivendo una quantità relativamente piccola di testo piuttosto raramente, quindi usa semplicemente un blocco statico e fallo con esso. Questa potrebbe essere la soluzione migliore in ogni caso, poiché l'unità disco può soddisfare solo una richiesta alla volta. Supponendo che tutti i file di output si trovino sulla stessa unità (forse non è un presupposto equo, ma si comportano con me), non ci sarà molta differenza tra il blocco a livello di applicazione e il blocco eseguito a livello di sistema operativo.

Quindi, se si dichiara locker come:

static object locker = new object(); 

Sarete essere certi che non vi siano conflitti con altri thread nel programma.

Se si desidera che questa cosa sia a prova di proiettile (o almeno ragionevolmente), non è possibile evitare le eccezioni. Le cose brutte possono accadere. È gestire le eccezioni in qualche modo. Quello che fai di fronte all'errore è qualcosa di completamente diverso. Probabilmente vorrai riprovare alcune volte se il file è bloccato. Se si ottiene un percorso errato o un errore di file o un disco pieno o una serie di altri errori, è probabile che si desideri terminare il programma. Di nuovo, dipende da te. Ma non puoi evitare la gestione delle eccezioni a meno che tu non stia bene con il crash del programma in caso di errore.

Tra l'altro, è possibile sostituire tutto questo codice:

   using (FileStream file = new FileStream(Filepath, FileMode.Append, FileAccess.Write, FileShare.Read)) 
       using (StreamWriter writer = new StreamWriter(file, Encoding.Unicode)) 
       { 
        writer.Write(text.ToString()); 
       } 

Con una sola chiamata:

File.AppendAllText(Filepath, text.ToString()); 

Supponendo che si sta utilizzando .NET 4.0 o versione successiva. Vedi File.AppendAllText.

Un altro modo in cui è possibile gestire questo è far scrivere ai thread i propri messaggi in una coda e disporre di un thread dedicato che servizi in coda. Avresti un BlockingCollection di messaggi e percorsi di file associati.Per esempio:

class LogMessage 
{ 
    public string Filepath { get; set; } 
    public string Text { get; set; } 
} 

BlockingCollection<LogMessage> _logMessages = new BlockingCollection<LogMessage>(); 

le discussioni che scrivono i dati a quella coda:

_logMessages.Add(new LogMessage("foo.log", "this is a test")); 

si avvia un processo in background di lunga durata che non fa nulla, ma il servizio quella coda:

foreach (var msg in _logMessages.GetConsumingEnumerable()) 
{ 
    // of course you'll want your exception handling in here 
    File.AppendAllText(msg.Filepath, msg.Text); 
} 

Il vostro potenziale Il rischio è che i thread creano messaggi troppo velocemente, causando la crescita della coda senza vincoli perché il consumatore non può tenere il passo. Se questo è un rischio reale nella tua applicazione è qualcosa che solo tu puoi dire. Se si ritiene che potrebbe essere un rischio, è possibile inserire una dimensione massima (numero di voci) sulla coda in modo che se le dimensioni della coda superano tale valore, i produttori attenderanno finché non ci sarà spazio nella coda prima di poter aggiungere.

+0

Grazie, molto da fare. parte del motivo per cui mi piace il mio grande utilizzo del blocco è che posso impostare FileShare.Read, che non sono sicuro sia il caso di File.AppendAllText. Ma di nuovo suppongo che sia solo un fattore se sono in competizione con altri processi. –

11

Si potrebbe anche usare ReaderWriterLock, si ritiene di essere più modo 'adeguato' per controllare la sicurezza dei thread quando si tratta di operazioni di lettura e scrittura ...

Per eseguire il debug le mie applicazioni web (quando debug remoto non riesce) Io uso in seguito ('debug.txt' finire nella cartella bin \ sul server):

public static class LoggingExtensions 
{ 
    static ReaderWriterLock locker = new ReaderWriterLock(); 
    public static void WriteDebug(string text) 
    { 
     try 
     { 
      locker.AcquireWriterLock(int.MaxValue); 
      System.IO.File.AppendAllLines(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Replace("file:\\", ""), "debug.txt"), new[] { text }); 
     } 
     finally 
     { 
      locker.ReleaseWriterLock(); 
     } 
    } 
} 

Spero che questo ti consente di risparmiare un po 'di tempo.

+0

Appena arrivato questo post, e mi ha aiutato, grazie @Matas! Ho pensato di informarvi che c'è un successore di ReaderWriterLock, chiamato ReaderWriterLockSlim, vedere https://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim(v=vs.110).aspx – KBoek

+0

ReaderWriterLock (e ReaderWriterLockSlim) sono per le situazioni in cui è necessario consentire più lettori e uno scrittore. In questo caso, se stai solo scrivendo su un file da più thread ma non stai leggendo, ti suggerisco di bloccare. – Jamie