2010-04-08 3 views
8

Mon application Web renvoie un fichier du système de fichiers. Ces fichiers sont dynamiques, donc je n'ai aucun moyen de connaître les noms o combien y en aura-t-il. Lorsque ce fichier n'existe pas, l'application le crée à partir de la base de données. Je veux éviter que deux threads différents recréent le même fichier en même temps, ou qu'un thread essaye de renvoyer le fichier pendant qu'un autre thread le crée.Comment verrouiller un fichier et éviter les lectures pendant l'écriture

De plus, je ne veux pas avoir de verrou sur un élément commun à tous les fichiers. Par conséquent, je devrais verrouiller le fichier juste quand je le crée. Donc je veux verrouiller un fichier jusqu'à ce que sa récréation soit terminée, si un autre thread essaye d'y accéder ... il devra attendre que le fichier soit déverrouillé.

J'ai lu sur FileStream.Lock, mais je dois connaître la longueur du fichier et cela n'empêchera pas cet autre thread d'essayer de lire le fichier, donc cela ne fonctionne pas dans mon cas particulier.

J'ai aussi lu sur FileShare.None, mais il va lancer une exception (quel type d'exception?) Si d'autres threads/processus essaient d'accéder au fichier ... donc je devrais développer un "try again while Faute "parce que je voudrais éviter la génération d'exception ... et je n'aime pas trop cette approche, bien que peut-être il n'y a pas de meilleure façon.

L'approche avec FileShare.None serait ce plus ou moins:

static void Main(string[] args) 
    { 
     new Thread(new ThreadStart(WriteFile)).Start(); 
     Thread.Sleep(1000); 
     new Thread(new ThreadStart(ReadFile)).Start(); 

     Console.ReadKey(true); 
    } 

    static void WriteFile() 
    { 
     using (FileStream fs = new FileStream("lala.txt", FileMode.Create, FileAccess.Write, FileShare.None)) 
     using (StreamWriter sw = new StreamWriter(fs)) 
     { 
      Thread.Sleep(3000); 
      sw.WriteLine("trolololoooooooooo lolololo"); 
     } 
    } 

    static void ReadFile() 
    { 
     Boolean readed = false; 
     Int32 maxTries = 5; 

     while (!readed && maxTries > 0) 
     { 
      try 
      { 
       Console.WriteLine("Reading..."); 
       using (FileStream fs = new FileStream("lala.txt", FileMode.Open, FileAccess.Read, FileShare.Read)) 
       using (StreamReader sr = new StreamReader(fs)) 
       { 
        while (!sr.EndOfStream) 
         Console.WriteLine(sr.ReadToEnd()); 
       } 
       readed = true; 
       Console.WriteLine("Readed"); 
      } 
      catch (IOException) 
      { 
       Console.WriteLine("Fail: " + maxTries.ToString()); 
       maxTries--; 
       Thread.Sleep(1000); 
      } 
     } 
    } 

Mais je n'aime pas le fait que je dois prendre des exceptions, essayez plusieurs fois et attendre une quantité inexacte de temps: |

+2

Il s'agit de FileShare.None au lieu de FileAccess.None (FileAccess définit l'accès à votre application, tandis que FileShare est utilisé pour verrouiller le fichier, comme vous le souhaitez) – jpabluz

+2

Sur un autre sujet, pensez à déverrouiller le fichier chaque fois que votre application est détruite , Je déteste quand le verrouillage reste après. – jpabluz

+0

J'ai modifié et corrigé, merci! – vtortola

Répondre

3

Vous pouvez gérer cela en utilisant l'argument FileMode.CreateNew au constructeur de flux. Un des threads va perdre et découvrir que le fichier a déjà été créé une microseconde plus tôt par un autre thread. Et obtiendra une IOException.

Il devra ensuite tourner, en attendant que le fichier soit entièrement créé. Lequel vous appliquez avec FileShare.None. Attraper des exceptions ici n'a pas d'importance, il tourne de toute façon. Il n'y a pas d'autre solution de contournement de toute façon, sauf si vous appelez P/Invoke.

+0

Il semble que vous avez raison, il n'y a pas d'autre solution pour cela. Merci! – vtortola

1

Je pense qu'un aproche droit serait la suivante: créer un ensemble de chaîne ont été u enregistrer le nom du fichier en cours si un thread traiterait le fichier à la fois, quelque chose comme ça

//somewhere on your code or put on a singleton 
static System.Collections.Generic.HashSet<String> filesAlreadyProcessed= new System.Collections.Generic.HashSet<String>(); 


//thread main method code 
bool filealreadyprocessed = false 
lock(filesAlreadyProcessed){ 
    if(set.Contains(filename)){ 
    filealreadyprocessed= true; 
    } 
    else{ 
    set.Add(filename) 
    } 
} 
if(!filealreadyprocessed){ 
//ProcessFile 
} 
+0

C'est le problème, que je ne veux pas verrouiller un élément commun pour tous les fichiers. Tout d'abord, obtenir un verrou est cher, et je ne veux pas obtenir un verrou pour chaque appel demandant un fichier, peu importe si le fichier existe déjà ou non.Deuxièmement, je ne veux pas bloquer les threads qui essaient d'obtenir un fichier différent parce que j'en crée un. Pour ces raisons, je veux verrouiller le fichier lui-même. Cheers. – vtortola

+0

Avez-vous mesuré le temps nécessaire à l'acquisition d'un verrou et au blocage jusqu'à la fin du fil, le contrôle de l'accès, l'obtention d'une exception, le sommeil et la répétition plusieurs fois? Je m'attends à ce qu'une stratégie de verrouillage soit plus souhaitable ici. Thread.Sleep est moins souhaitable de bloquer sur un verrou. Que faire si le thread d'écriture se termine tôt? Le fil de lecture ne se réveille pas. Vous pourriez envisager un 'ManualResetEvent' pour contrôler l'accès entre les deux threads. –

1

Avez-vous un moyen d'identifier quels fichiers sont créés?

Dites que chacun de ces fichiers correspond à un ID unique dans votre base de données. Vous créez un emplacement centralisé (Singleton?), Où ces ID peuvent être associés à quelque chose verrouillable (Dictionary). Un fil qui a besoin de lire/écrire à l'un de ces fichiers effectue les opérations suivantes:

//Request access 
ReaderWriterLockSlim fileLock = null; 
bool needCreate = false; 
lock(Coordination.Instance) 
{ 
    if(Coordination.Instance.ContainsKey(theId)) 
    { 
     fileLock = Coordination.Instance[theId]; 
    } 
    else if(!fileExists(theId)) //check if the file exists at this moment 
    { 
     Coordination.Instance[theId] = fileLock = new ReaderWriterLockSlim(); 
     fileLock.EnterWriteLock(); //give no other thread the chance to get into write mode 
     needCreate = true; 
    } 
    else 
    { 
     //The file exists, and whoever created it, is done with writing. No need to synchronize in this case. 
    } 
} 

if(needCreate) 
{ 
    createFile(theId); //Writes the file from the database 
    lock(Coordination.Instance) 
     Coordination.Instance.Remove[theId]; 
    fileLock.ExitWriteLock(); 
    fileLock = null; 
} 

if(fileLock != null) 
    fileLock.EnterReadLock(); 

//read your data from the file 

if(fileLock != null) 
    fileLock.ExitReadLock(); 

Bien sûr, les discussions qui ne suivent pas ce protocole de verrouillage exact aura accès au fichier. Maintenant, le verrouillage sur un objet Singleton n'est certainement pas idéal, mais si votre application a besoin d'une synchronisation globale, alors c'est un moyen d'y parvenir.

+0

Même problème que le code @hworangdo, dans chaque requête vous devez acquérir un verrou, même lorsque vous n'en avez pas besoin. – vtortola

+1

@vtortola: Oui. En défense de ma réponse: Obtenir un verrou n'est pas cher, (mesurez-le, c'est vraiment rien, surtout par rapport au fichier IO) mais attendez qu'un autre thread libère le verrou. Vous pouvez essayer de trouver une implémentation de dictionnaire sans verrou. Vous devez seulement faire attention dans le cas où le fichier doit être créé, de sorte qu'un seul thread soit chargé de le créer. –

+0

Vous avez probablement raison, je n'ai jamais testé comment expesive est l'acquisition d'un verrou moi-même, je le sais parce que je l'ai lu. L'attente d'un autre thread pour libérer le verrou est plus onéreuse, mais cela n'arrivera qu'une fois par fichier. Je vais tester votre approche plus tard, peut-être que le vôtre est plus rapide. Merci! – vtortola

1

Votre question m'a vraiment fait réfléchir.Au lieu d'avoir chaque thread responsable de l'accès aux fichiers et de les bloquer, que se passe-t-il si vous avez utilisé une file de fichiers qui doivent être persistants et avoir un seul thread de travail d'arrière-plan persister? Pendant que le travail d'arrière-plan démarre, les threads de l'application Web peuvent renvoyer les valeurs de base de données jusqu'à ce que le fichier existe réellement. J'ai posté un très simple example of this on GitHub.

N'hésitez pas à faire un essai et dites-moi ce que vous en pensez.

Pour votre information, si vous n'avez pas git, vous pouvez utiliser svn pour le tirer http://svn.github.com/statianzo/MultiThreadFileAccessWebApp

0

Pourquoi n'êtes-vous pas simplement en utilisant la base de données - par exemple Si vous avez un moyen d'associer un nom de fichier aux données de la base de données qu'il contient, ajoutez simplement des informations à la base de données qui spécifie si un fichier existe avec cette information et quand il a été créé, comment les informations contenues dans le fichier sont obsolètes Lorsqu'un thread a besoin d'informations, il vérifie si le fichier existe et, sinon, il écrit une ligne dans la table indiquant qu'il crée le fichier. Quand c'est fait, il met à jour cette ligne avec un booléen indiquant que le fichier est prêt à être utilisé par d'autres.

la bonne chose à propos de cette approche - toutes vos informations sont en 1 place - de sorte que vous pouvez faire une belle récupération d'erreur - par exemple. Si le thread qui crée le fichier ne fonctionne pas correctement pour une raison quelconque, un autre thread peut venir et décider de réécrire le fichier car le temps de création est trop ancien. Vous pouvez également créer des processus de nettoyage par lots simples et obtenir des données précises sur la fréquence d'utilisation de certaines données pour un fichier, la fréquence de mise à jour des informations (en examinant les heures de création, etc.). De plus, vous évitez de faire beaucoup de recherches de disques sur votre système de fichiers car différents threads recherchent des fichiers différents partout - en particulier si vous décidez d'avoir plusieurs machines frontales recherchées sur un disque commun. La chose délicate - vous devrez vous assurer que votre db supporte le verrouillage au niveau de la ligne sur la table à laquelle les threads écrivent lorsqu'ils créent des fichiers, sinon la table elle-même pourrait être verrouillée ce qui pourrait ralentir de façon inacceptable.

0

La question est ancienne et il y a déjà une réponse marquée. Néanmoins, je voudrais poster une alternative plus simple.

Je pense que nous pouvons directement utiliser l'instruction de verrouillage sur le nom du fichier, comme suit:

lock(string.Intern("FileLock:absoluteFilePath.txt")) 
{ 
    // your code here 
} 

En général, le verrouillage d'une chaîne est une mauvaise idée à cause de chaîne interner. Mais dans ce cas particulier, il devrait s'assurer que personne d'autre n'est en mesure d'accéder à ce verrou. Utilisez simplement la même chaîne de verrouillage avant d'essayer de lire. Ici, l'internement fonctionne pour nous et non contre. PS: Le texte 'FileLock' est juste un texte arbitraire pour s'assurer que les autres chemins de fichier chaîne ne sont pas affectés.

Questions connexes