web-dev-qa-db-fra.com

Écrire dans un fichier en toute sécurité

Ecrire Stringbuilder dans un fichier de façon asynchrone. Ce code prend le contrôle d'un fichier, y écrit un flux et le libère. Il traite les demandes d'opérations asynchrones, qui peuvent arriver à tout moment.

FilePath est défini par instance de classe (donc le verrou Object est par instance), mais il y a un risque de conflit car ces classes peuvent partager FilePaths. Ce type de conflit, ainsi que tous les autres types extérieurs à l'instance de classe, seraient traités avec des tentatives.

Ce code est-il adapté à son objectif? Y a-t-il une meilleure façon de gérer cela qui signifie moins (ou pas) de dépendance à l'égard du mécanisme de capture et de nouvelle tentative?

Aussi comment éviter d'attraper des exceptions qui se sont produites pour d'autres raisons.

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();
    }
26
Nathan Cooper

Votre approche dépendra beaucoup de la fréquence à laquelle vous écrivez. Si vous écrivez une quantité relativement petite de texte assez rarement, utilisez simplement un verrou statique et finissez-en. C'est peut-être votre meilleur pari dans tous les cas, car le lecteur de disque ne peut satisfaire qu'une seule demande à la fois. En supposant que tous vos fichiers de sortie se trouvent sur le même lecteur (ce n'est peut-être pas une hypothèse raisonnable, mais restez avec moi), il n'y aura pas beaucoup de différence entre le verrouillage au niveau de l'application et le verrouillage effectué au niveau du système d'exploitation.

Donc, si vous déclarez locker comme:

static object locker = new object();

Vous serez assuré qu'il n'y a aucun conflit avec d'autres threads dans votre programme.

Si vous voulez que cette chose soit à l'épreuve des balles (ou du moins raisonnablement), vous ne pouvez pas échapper aux exceptions. De mauvaises choses peuvent arriver. Vous devez gérer les exceptions d'une manière ou d'une autre. Ce que vous faites face à l'erreur est tout autre chose. Vous voudrez probablement réessayer plusieurs fois si le fichier est verrouillé. Si vous obtenez un mauvais chemin d'accès ou une erreur de nom de fichier ou un disque plein ou l'une des nombreuses autres erreurs, vous souhaiterez probablement tuer le programme. Encore une fois, cela dépend de vous. Mais vous ne pouvez pas éviter la gestion des exceptions, sauf si vous êtes d'accord avec le programme qui plante en cas d'erreur.

Au fait, vous pouvez remplacer tout ce code:

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

Avec un seul appel:

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

En supposant que vous utilisez .NET 4.0 ou version ultérieure. Voir File.AppendAllText .

Une autre façon de gérer cela est de demander aux threads d'écrire leurs messages dans une file d'attente et d'avoir un thread dédié qui dessert cette file d'attente. Vous auriez un BlockingCollection de messages et les chemins de fichiers associés. Par exemple:

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

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

Vos threads écrivent des données dans cette file d'attente:

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

Vous démarrez une tâche d'arrière-plan de longue durée qui ne fait que servir cette file d'attente:

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

Votre risque potentiel ici est que les threads créent des messages trop rapidement, ce qui entraîne une augmentation de la file d'attente sans limite car le consommateur ne peut pas suivre. Que ce soit un risque réel dans votre application, vous seul pouvez le dire. Si vous pensez que cela pourrait représenter un risque, vous pouvez mettre une taille maximale (nombre d'entrées) dans la file d'attente afin que si la taille de la file d'attente dépasse cette valeur, les producteurs attendent qu'il y ait de la place dans la file d'attente avant de pouvoir ajouter.

39
Jim Mischel

Vous pouvez également utiliser ReaderWriterLock , il est considéré comme un moyen plus "approprié" de contrôler la sécurité des threads lors des opérations de lecture/écriture ...

Pour déboguer mes applications Web (lorsque le débogage à distance échoue), j'utilise ce qui suit ('debug.txt' se retrouve dans le dossier\bin sur le serveur):

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

J'espère que cela vous fera gagner du temps.

18
Matas Vaitkevicius