5

With this code for a very basic logger:

lock (string.Concat("LogWritter_", this.FileName))
{
    using (var fileStream = File.Open(this.FileName, FileMode.Append, FileAccess.Write, FileShare.Read))
    {
        using (var w = new StreamWriter(fileStream))
        {
            w.Write(message);
        }
    }
}

when I try it from a few threads simultaneously I quickly get the error:

The process can't access the file because its being used by another file.

Why the lock is not preventing the threads to access the file at the same time?

It doesn't matter if the threads call the same instance or different instances to the same file. Also I thought It could be because of some deferral when writing files in Windows, but on Linux happens the same thing.

Santiago Corredoira
  • 47,267
  • 10
  • 52
  • 56

5 Answers5

12

You are locking on a temporary string. You have to introduce a static object to lock on.

empi
  • 15,755
  • 8
  • 62
  • 78
  • 2
    Shouldn't the string be immutable / stored only once no matter how many times it's made? – zimdanen May 04 '12 at 17:21
  • Why is immutability relevant? – spender May 04 '12 at 17:22
  • @zimdanen each of the many distinct strings that are created are immutable. Mutability is not the issue. – David Heffernan May 04 '12 at 17:22
  • 1
    @zimdanen, two strings with the same value do not necessarily have the same reference. Locking is based on the actual reference. – Dan Bryant May 04 '12 at 17:22
  • I think the idea here is to lock access to a each file, so a single lock object would not suffice. – spender May 04 '12 at 17:23
  • @DanBryant - Do you have a link that explains that? I was under the impression that two strings with the same value **do** have the same reference. – zimdanen May 04 '12 at 17:25
  • 1
    http://msdn.microsoft.com/en-us/library/system.string.intern.aspx suggests that not all strings are interned. – spender May 04 '12 at 17:28
  • @zimdanen - you're thinking about string interning – empi May 04 '12 at 17:28
  • @spender/empi - Thanks. So this should work for the OP, then? lock (string.Intern(string.Concat("LogWritter_", this.FileName))) – zimdanen May 04 '12 at 17:33
  • 1
    http://blogs.msdn.com/b/tess/archive/2009/10/19/net-hang-case-study-the-danger-of-locking-on-strings.aspx – spender May 04 '12 at 17:34
  • 1
    @zimdanen: that will work unless someone else in all the other code in your appdomain has the same idea and locks on the same string. It will also leak memory, as interning is a one-way operation. Too risky for me. – spender May 04 '12 at 17:35
  • @zimdanen: It should work, but I don't think it's a good idea. I would go with spender's suggestion. – empi May 04 '12 at 17:36
  • @SCJ: I don't want to update my answer since it got accepted and got some upvotes, but my opinion is that you shouldn't rely on string interning and be careful with creating Dictionary of objects to lock on. That dictionary is not trivial to implement properly and it solves your problem partially. You will still get performance problems if you always write to file and don't keep some kind of buffer. The bottom line is, consider using some logging framework, eg. log4net. – empi May 04 '12 at 17:59
  • @empi, yes, it was trickier than I thought at first, but I have it working already . About buffering I need it to log errors so I want them to be written to disk as soon as they occur. – Santiago Corredoira May 04 '12 at 18:08
  • @SCL: you need lock on a static object like `public static object _syncRoot` – dance2die May 09 '12 at 18:39
8

Create a Dictionary<string,object> and store your lock objects there with the filepath as key.

A while back, I approached this same question:

Locking by string. Is this safe/sane?

Community
  • 1
  • 1
spender
  • 117,338
  • 33
  • 229
  • 351
  • 2
    Although this answer misses some context, this is the right way to do it if you don't want to have a single lock for multiple files. Look for instance at [this implementation](http://logging.codeplex.com/SourceControl/changeset/view/72677#1298869) of some [logging framework](http://logging.codeplex.com). That implementation uses a `OrdinalIgnoreCase` `Dictionary` and ensures that canonical file paths are used (important). – Steven May 04 '12 at 17:28
5

The C# lock statement places a lock on the object, not the uniqueness of the string. So, because you are dynamically concatenated two strings, you are essentially creating a new object every single time, so every single lock is unique. Even if you access the same file every time, "A" + "B" results in some new immutable string; "A" + "B" again results in yet another new object.

TheBuzzSaw
  • 8,648
  • 5
  • 39
  • 58
4

You are only locking a dynamically created string ("LogWritter_" + this.FileName)! Each thread will create another one. Create a common lock-object instead

public static readonly object fileLock = new object();

...

lock (fileLock) {
    ...
}

If you want to create different locks for different files, you will have to store them in a collection that will be used by all the threads.

If you are working with .NET Framework 4.0, you can use a ConcurrentDictionary<TKey, TValue>. Otherwise you will have to lock the access to a normal Dictionary<TKey, TValue>

public static readonly ConcurrentDictionary<string,object> fileLocks =
    new ConcurrentDictionary<string,object>();

...

object lockObject = fileLocks.GetOrAdd(filename, k => new object());
lock (lockObject) {
    ...
}

UPDATE

If you want to compare the references of two strings, you have to use

Object.ReferenceEquals(s1, s2)

Where

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // ===> true

string s3 = s1 + " World!";
string s4 = s2 + " World!";
Console.WriteLine(s3 == s4); // ===> true
Console.WriteLine(Object.ReferenceEquals(s3, s4)); // ===> false

Strings created at compile-time are interned, i.e. a single string constant will be created for equal strings. Strings created at runtime, however, will be created as individual and distinct objects!

The hash code of strings is calculated from the characters of the string, not from their reference.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
1

Try out this code. When the first thread comes in and calculates the value of string.Concat("LogWritter_", this.FileName) it puts a lock on this string. The second thread will also calculates the same string value but the strings are going to be different. If you will compare strings using ==,Equals() or GetHashCode() you will see both strings are same because == and Equals() are overloaded for string class. but if you will check the ReferenceEquals() then you it returns false. It means both the string have different references. And that is why first thread locks on one string object and second thread locks on second string object and you get the error.

class Program
{
    public static void Main(string[] args)
    {
        string locker = "str", temp = "temp";
        string locker1 = locker + temp;
        string locker2 = locker + temp;

        Console.WriteLine("HashCode{0} {1}", locker1.GetHashCode(), locker2.GetHashCode());
        Console.WriteLine("Equals {0}", locker1.Equals(locker2));
        Console.WriteLine("== {0}", locker1 == locker2);
        Console.WriteLine("ReferenceEquals {0}", ReferenceEquals(locker1, locker2));
        app.Program p = new Program();
        Action<string> threadCall = p.Run;
        threadCall.BeginInvoke(locker1, null, null);
        threadCall.BeginInvoke(locker2, null, null);
        Console.Read();
    }

    public void Run(string str)
    {
        lock (str)
        {
            Console.WriteLine("im in");
            Thread.Sleep(4000);
            Console.WriteLine("print from thread id {0}", Thread.CurrentThread.ManagedThreadId);
        }
    }


}
mchicago
  • 780
  • 9
  • 21