Matthew
Matthew

Reputation: 10444

How to ensure a task has started before continuing?

I have a method that populates a shared collection and I invoke it in a lock and in a Task like this:

void PopulateCollection()
{
    Task.Factory.StartNew(() =>
        {
            lock (_padlock)
            {
                _sharedDictionary = GetSharedDictionary();
            }
        });
}

I use the same lock(_padlock) around that collection whenever it's read.

The problem I'm having is that the Task.Factory may not have started its task (and so the lock may not have been obtained). This results in a race condition.

A reader method like this exists:

void ReadCollection()
{
    lock(_padlock)
    {
        DoSomethingWithCollection(_sharedDictionary);
    }
}

So my problem is that I have code like this:

...
PopulateCollection();
... // some things happen
ReadCollection();
...

I have no guarantee that the collection is populated before it's read because I cannot guarantee that the Task has started (and thus the lock has been obtained) before the collection is read.

I don't want to proceed until that lock has been obtained.

Upvotes: 0

Views: 222

Answers (3)

Servy
Servy

Reputation: 203812

You're not using the TPL properly to begin with, hence your problem. You generally shouldn't need to be using lock very much at all when using the TPL.

Rather than mutating a shared variable within your task to set the result of an operation, have the result of that operation set the result of the task. You can then hold onto a reference to that task and use its value when it is computed.

Task<Dictionary<TKey,TValue>> PopulateCollection()
{
    return Task.Factory.StartNew(() => GetSharedDictionary());
}

You can then attach a continuation to that task to do something with the result after it is computed:

PopulateCollection()
    .ContinueWith(task => DoSomethingWithCollection(t.Result));

Upvotes: 4

svick
svick

Reputation: 245038

What I would do is to return the Task, so that you can wait for the operation to complete, not for it to start, as you suggested. Something like:

Task PopulateCollection()
{
    return Task.Factory.StartNew(() =>
        {
            lock (_padlock)
            {
                _sharedDictionary = GetSharedDictionary();
            }
        });
}
var populateTask = PopulateCollection();
... // some things happen
populateTask.Wait();
ReadCollection();

If you populate the collection only once and then use it repeatedly, another option would be to change _sharedDictionary to Task<YourDictionaryType>:

void PopulateCollection()
{
    _sharedDictionaryTask = Task.Factory.StartNew(() => GetSharedDictionary());
}

void ReadCollection()
{
    DoSomethingWithCollection(_sharedDictionaryTask.Result);
}

Task will take care of the necessary synchronization here.

Upvotes: 2

Edin
Edin

Reputation: 1496

If you wish to ensure, that the lock inside the task is obtained before the continuation of the flow outside the task, you can use AutoResetEvent:

   var waitHandle = new AutoResetEvent(false);
    Task.Factory.StartNew(() =>
        {
            lock (_padlock)
            {
               waitHandle.Set();
                _sharedDictionary = GetSharedDictionary();
            }
        });
   waitHandle.WaitOne();

Upvotes: 1

Related Questions