4

Given a method that writes to a text file

public void WriteToFile( ) {
    var file = "C:\\AsyncTest.txt";
    var writer = File.Exists( file ) ? File.AppendText( file ) : File.CreateText( file );
    writer.WriteLine( "A simulated entry" );
    writer.Close();
}

I need to simulate a scenario in which this method could be called in a loop, possibly dozens of times and must run asynchronously.

So I tried calling the method in a new thread like so (where writer is the class where WriteToFile lives)

//in a loop...
  Thread thread = new Thread( writer.WriteToFile );
  thread.Start(  );

Which works perfectly once, but throws an IO Exception that the file is being used by another process on subsequent iterations. Which makes perfect sense, actually, but I don't know how to work around it.

I tried using Join() like this

Thread thread = new Thread( writer.WriteToFile );
thread.Start(  );
thread.Join();

But that locks the calling thread until all the joined threads complete, which sort of defeats the purpose, no?

I tried using ThreadPool.QueueUserWorkItem(writer.WriteToFile);, but get the same IO exception.

I tried using a lock

private object locker = new object();

public void WriteToFile( ) {
  lock(locker){
    //same code as above
  }
}

But that had no apparent effect

I also tried using the Task class to no avail.

So how can I "stack up" these background threads to write to a single file without conflict, while not locking up the calling thread?

Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
Forty-Two
  • 7,535
  • 2
  • 37
  • 54
  • Try locking inside the thread on a global (static) variable, and adding a slight sleep (windows-problems with opening and closing files) just before the lock ends (like 100ms). – Alxandr Aug 01 '13 at 12:35
  • One file or multiple files? What you want to write is fixed or passed as a parameter? – xanatos Aug 01 '13 at 12:37
  • http://stackoverflow.com/questions/10519853/writing-to-file-file-being-used-by-another-process Would this help for unlocking the file? – Dan Drews Aug 01 '13 at 12:38
  • Use tasks. You can then use [ContinueWith](http://msdn.microsoft.com/en-us/library/dd270696.aspx) to queue up the next action, and so on. – Lasse V. Karlsen Aug 01 '13 at 12:43
  • You don't open the file in a way that it can write to the same file asynchronously. – Security Hound Aug 01 '13 at 14:31

4 Answers4

11

Another option is to create a queue. Have the main thread put strings on the queue and have a persistent background thread read the queue and write to the file. It's really easy to do.

private BlockingCollection<string> OutputQueue = new BlockingCollection<string>();

void SomeMethod()
{
    var outputTask = Task.Factory.StartNew(() => WriteOutput(outputFilename),
        TaskCreationOptions.LongRunning);

    OutputQueue.Add("A simulated entry");
    OutputQueue.Add("more stuff");

    // when the program is done,
    // set the queue as complete so the task can exit
    OutputQueue.CompleteAdding();

    // and wait for the task to finish
    outputTask.Wait();
}

void WriteOutput(string fname)
{
    using (var strm = File.AppendText(filename))
    {
        foreach (var s in OutputQueue.GetConsumingEnumerable())
        {
            strm.WriteLine(s);
            // if you want to make sure it's written to disk immediately,
            // call Flush. This will slow performance, however.
            strm.Flush();
        }
    }
}

The background thread does a non-busy wait on the output queue, so it's not using CPU resources except when it's actually outputting the data. And because other threads just have to put something on the queue, there's essentially no waiting.

See my blog, Simple Multithreading, Part 2 for a little bit more information.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • Wouldn't you know it. I got the lock working and my team decided that locks are bad and want to go with a queue...:) (Great blog, btw!) – Forty-Two Aug 01 '13 at 19:34
  • @Forty-Two: Glad you got it going. Thanks for the good word. And whereas I agree that locks can be misused, the mantra "locks are bad" can be taken to unrealistic extremes. See my blog [Unwise conventional wisdom, Part 1: Locks are slow](http://blog.mischel.com/2010/09/21/unwise-conventional-wisdom-part-1-locks-are-slow/) – Jim Mischel Aug 01 '13 at 20:04
4

You could use something like:

// To enqueue the write
ThreadPool.QueueUserWorkItem(WriteToFile, "A simulated entry");

// the lock
private static object writeLock = new object();

public static void WriteToFile( object msg ) {
    lock (writeLock) {
        var file = "C:\\AsyncTest.txt";

        // using (var writer = File.Exists( file ) ? File.AppendText( file ) : File.CreateText( file )) {
        // As written http://msdn.microsoft.com/it-it/library/system.io.file.appendtext(v=vs.80).aspx , File.AppendText will create the
        // file if it doesn't exist

        using (var writer = File.AppendText( file )) {
            writer.WriteLine( (string)msg );
        }
    }
}

And please, use using with files!

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Hey, thanks. This works -- at least in this simulation -- hopefully it'll translate to the actual app:). But would you mind explaining why it works if the lock object and write method are static, but fails if they are not (as in TheSolution's answer)? – Forty-Two Aug 01 '13 at 13:13
  • By the way, there's no need for the conditional. `File.AppendText` will create a new file if one doesn't exist. So you could just write `using (var writer = File.AppendText(file))` – Jim Mischel Aug 01 '13 at 18:34
2

You could handle the locking like you tried, but you need to include the using statement:

private readonly object _lockObj = new Object();

public void WriteToFile( )
{
    var file = "C:\\AsyncTest.txt";
    lock (_lockObj)
    {
        using (StreamWriter writer = File.AppendText( file ))
        {
            writer.WriteLine( "A simulated entry" );
        }
    }
}

Further, you don't need to leverage CreateText because AppendText will create the file if it doesn't exist. Finally, I've had problems with this code before as well in that the lock will be released before Windows releases the resource. It's rare, but it happens, so I just add a little retry logic looking for that specific exception.

Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
2
  1. Dispose your streams.
  2. Use TPL or async/await capabilities.

For example:

Task.Run(() => File.WriteAllText("c:\\temp\test.txt", "content"));

This will run a write operation asynchronously without you taking care of threads.

Also, streams and stream writers provide WriteAsync methods that you can use and await for.

UPDATE

To avoid "locking" problem simply don't do locking :) Locking happens if you try to write to the same file from different threads. You may use File.Open() methods and specify the mode so it will block a thread and wait until file is writable.

But blocking is bad. So I advice you, in case if you want to write from multiple threads, to create a queue and put your writing tasks into this queue. You can put safely from multiple thread (use ConcurrentQueue<T>). Then you consume this queue in a background task and just write to your file what you have in the queue - one item by one.

That's it: multiple publishers, one [file writing] consumer, super easy, no locks required.

Alexey Raga
  • 7,457
  • 1
  • 31
  • 40
  • You aren't solving his problem of locking – xanatos Aug 01 '13 at 12:54
  • You don't have any problems of locking in case if you dispose of your streams (and .WriteAllText does it internally) and if you don't write to the same file at the same time from different threads. – Alexey Raga Aug 01 '13 at 13:03
  • If you do want to write the same file from diff threads I would advice against locking. Will update my answer. – Alexey Raga Aug 01 '13 at 13:04
  • Sorry, should have mentioned I'm working with .NET 4.0 -- I think Task.Run was introduced in 4.5? – Forty-Two Aug 01 '13 at 13:18
  • 1
    @AlexeyRaga `You may use File.Open() methods and specify the mode so it will block a thread and wait until file is writable.` This I don't think you can do (with using only `File.Open`) – xanatos Aug 01 '13 at 13:22
  • Use TaskFactory.StartNew(() => ) then if Task.Run is unavailable. They are equivalent (as if you provide TaskFactory with no cancellation token etc) – Alexey Raga Aug 01 '13 at 13:29