views:

120

answers:

2

In this code, I have an Entity Framework 4 model with a single "Thing" entity that has an Id and a Name(string) column. I would like to ensure that when I call FindOrCreateThing(name) from multiple threads, only one row in the Things table will ever be created with the given name.

Currently, I'm using locks to accomplish this, and it seems to work... but, what are some better ways? How is this common scenario handled in other projects?

Thanks!

class Program
{
    private static string[] names = new string[] { "Alpha", "Beta", "Delta", "Gamma", "Zeta" };

    static void Main(string[] args)
    {
        // Multiple threads trying to create things, often with the same name,
        // but only allow one thread to actually create the record if it doesn't
        // exist.
        for (int i = 0; i < 100; i++)
        {
            Thread thread = new Thread(new ThreadStart(MakeThings));
            thread.Start();
        }
    }

    static void MakeThings()
    {
        try
        {
            foreach (var name in names)
            {
                Thing t = FindOrCreateThing(name);
                Console.WriteLine("Thing record returned: id={0}; name={1}", t.Id, t.Name);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    private static object createLock = new object();
    private static Thing FindOrCreateThing(string name)
    {
        using (EFModel context = new EFModel())
        {
            // Find the record. If it already exists, return it--we're done.
            var thing = (from t in context.Things
                         where t.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase)
                         select t).SingleOrDefault();
            if (thing == null)
            {
                // The record does not exist, so wait for lock.
                // This will prevent multiple threads from trying to
                // create the same record simultaneously.
                lock (createLock)
                {
                    // Single thread is here... check if a thread before us
                    // has already created the record. (I hate having to do this
                    // same query twice!)
                    thing = (from t in context.Things
                             where t.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase)
                             select t).SingleOrDefault();
                    if (thing == null)
                    {
                        // We're the first thread here, so create the record.
                        // This should mean that the record is unique in the table.
                        thing = new Thing { Name = name };
                        context.Things.AddObject(thing);
                        context.SaveChanges();
                    }
                }
            }
            return thing;
        }
    }
}
+1  A: 

Just put a unique constraint on the DB column. You can then get rid of all of the locking, and catch the (unlikely, but possible) exception you'll get if you search, find nothing, and create, while another thread does the same thing. If you catch that, just retry the whole process.

Craig Stuntz
"Retry the whole process" sounds less efficient than waiting for a lock.Would a thread-safe stored procedure work better for this scenario?
Eric Baker
An application-based lock is inherently broken if your app is running on a server farm. The retry is a *rare* event; don't prematurely optimize for something which happens only in the (relatively) unlikely contention scenario. The overhead of the `try` block when no exception is actually thrown is close to nil. No, a proc is not a good solution as it can't see outside of transaction isolation. Don't try to reinvent unique constraints; just use them.
Craig Stuntz
@Craig: Ok, that makes sense. Thanks for your advice.
Eric Baker
A: 

If the store behind the entity is some RDBMS, then it would probably be more efficient to create a unique index on the column. It would reduce round trips to the server and you would not need to do locking at the client side.

Mark Wilkins
Yes, its MS SqlServer 2008. Sorry I didn't specify that.
Eric Baker
@Eric: Creating a unique index then should work fine for this. Or as Craig suggested, a unique constraint (which I think does the same thing underneath ... or so I have read).
Mark Wilkins