2

I am writing a class that contains data. It exposes methods that allow to query the data, while the data is also being updated from an external source (web service, for example).

All the methods expose tasks that are started on a worker thread using the default task scheduler. So the structure of the class is as follows:

public class A
{
    private object _myLock;

    public Task<int> GetSomeNumber(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            { 
                int result = 0;
                // calculate the result...
                // ...
                return result;
            }
        });
    }

    public Task<string> GetSomeText(int aParameter)
    {
        return Task.Run(() =>
        {
            lock (_myLock)
            {
                string result = "";

                // calculate the result...
                // ...
                return result;
            }
        });
    }
}

The methods may be called several multiple times, while the data itself is being updated, so the calculations that require the shared data are surrounded with a lock statement. The problem is that this code blocks the thread in which the task is run. Of course, these are not UI thread, so the UI remains responsive, but it does consume threads and keeps them blocked. I would prefer for the worker threads in the thread pool to remain as available as possible.

Is there a way, a best practice of some sort, that allows to write this code so that the shared data is thread safe and at the same time the tasks are not blocked? If instead of waiting for a lock, the tasks would need to wait for something else to finish (a task), I would use ContinueWith in order to create a continuation task and return it, so that it would be scheduled to run when the first task is completed. Is there a way to do the same with lock, so that the task is only scheduled when the lock is available?

Update I have done some reading and found out about several possible options that allow for shared data sync. These include: SemaphoreSlim, AsyncLock, and ConcurrentExclusiveSchedulerPair.

I would love to see a recommendation for a best practice, of writing a service class that provides "read only" task methods that can run concurrently with a read lock, and "write" task methods that require write lock.

5
  • there are numerous solutions. but we need a more detailed explanation of the problem.... what kind of data is it? can it be immutable to a degree? what calculations are you doing on it?
    – AK_
    Commented Feb 16, 2015 at 12:24
  • @AK_, It is not the sort of data that you can keep in concurrent lists or dictionary. I was thinking about the read/write lock approach, only without blocking (as much as possible, using tasks that may be scheduled to run in a way that prevents concurrent writes). The only "catch" is that "read" tasks should sometime trigger "write" tasks and wait for them to finish, as well.
    – Kobi Hari
    Commented Feb 16, 2015 at 16:31
  • what is your usecase? what are you actually doing?
    – AK_
    Commented Feb 16, 2015 at 16:48
  • 1
    We have a web service the provides graph nodes. We hold a local copy of part of the graph and expose and async API to query the graph. If the area that you query is in the cache then we simply calculate and return the result, otherwise we start downloading the missing part and then perform the calculation and return the result. There is also a recurring task in the background that polls the server for changes in the node and applies the changes to the local cache.
    – Kobi Hari
    Commented Feb 16, 2015 at 18:21
  • @KobiHari Please include your use case description in the question. Without that description, your question would be too broad to be answered.
    – rwong
    Commented Mar 21, 2015 at 7:18

3 Answers 3

2

This answer is based on the additional information about the use case mentioned in the comments.

We have a web service the provides graph nodes. We hold a local copy of part of the graph and expose and async API to query the graph. If the area that you query is in the cache then we simply calculate and return the result, otherwise we start downloading the missing part and then perform the calculation and return the result. There is also a recurring task in the background that polls the server for changes in the node and applies the changes to the local cache.


One should realize that if a request cannot be completed right away because the data needed to fulfill the request is simply not available (e.g. haven't been downloaded or cached), there are only a few choices:

  • Return a failure right away. This is similar to the TryGet pattern, which does not block - but the task won't succeed.
  • Return a failure right away, but also indicate that it has started an asynchronous request to fetch data from the external server. This way, the reader won't block, but will have to retry at a later time.
  • Require the reader to provide a callback function, which will be called later when the data is available. This can be combined with the asynchronous request approach.
  • Block the reader until the data has been fetched.

Each of the options above has its own set of "best practices" and integration patterns with TPL. It would be too much to discuss all of them here.

Help needed: If anyone knows of good online resource that covers the best practices for these options, please post as comments.


If you decide that blocking the reader is the only choice given the requirements of your application, keep in mind that:

  • The .NET default thread pool is elastic - meaning that it will launch more worker threads if it senses that some threads are being blocked. This is good. It means that blocking any number of readers will not result in system-wide failure.
  • If it is not elastic, your application may deadlock due to worker thread starvation (exhaustion).
  • You must therefore never change the settings on the default thread pool that will cause it to lose the elasticity - unless you are willing to reboot your service whenever that happens.
    • For example, do not set a maximum number of worker threads.

If you want the concurrent readers to never get stuck (being obstruction free), I think you have several choices:


Multiple buffering

Link to Wikipedia article

  • The system maintains multiple copies of the graph, and rotate (cycle through) them whenever a modification is needed.
  • At any point in time, at most one copy will be modified. All other copies will be read-only.
  • When a modification is taking place, each reader must choose between:
    • Reading from one of the other read-only, slightly out-of-date copies of the graph. Multiple readers can access it concurrently.
    • Wait until the modification is complete, to make sure it will have access to the most up-to-date version of the graph.

Concurrent graph update algorithms

The exact details will depend on what operations and algorithms are needed for the application.

Some of them may be lock-free, using the hardware-enabled atomic instructions. Others may use various kinds of locks, but may try to minimize the probability or the length of time where readers might be blocked.

One may either use an existing library or implement one's own.

I'm not familiar with concurrent graph update algorithms, so I can't give much suggestions.

0

How about:

// any child object should freeze when this object freezes...
interface IFreazableCloanable
{
    bool IsFrozen { get; }
    // Any further attempts to change the object should throw an Exception
    void Freeze();

    // Creates mutable Clone of the object
    IFreazableCloanable DeepCloneMutable();
}

class SyncedObject<T> where T : class, IFreazableCloanable
{
    SemaphoreSlim _writersLock = new SemaphoreSlim(0, 1);
    volatile T _internal;

    public SyncedObject(T thing) // if writes are unsynced then only the last one counts.
    {
        _internal = (T)thing.DeepCloneMutable();
    }
    public T Read()
    {
        T result = _internal;

        result.Freeze();

        return result;
    }

    public void Write(Func<T, T> writer)
    {
        _writersLock.Wait();

        T temp = (T)_internal.DeepCloneMutable();

        temp = writer(temp);

        _internal = temp;

        _writersLock.Release();
    }
}

class Data : IFreazableCloanable
{
    private string _data = "lala";
    public string MyData
    {
        get
        {
            return _data;
        }
        set
        {
            if (IsFrozen)
            {
                throw new Exception("Very Bad");
            }
            _data = value;
        }
    }
    public bool IsFrozen
    {
        get;
        private set;
    }

    public void Freeze()
    {
        IsFrozen = true;
    }

    public IFreazableCloanable DeepCloneMutable()
    {
        return new Data();
    }
}
3
  • AK thanks but as I said, we do not want the data to be imutable. Non of the queries return the data itself anyway. It remains private. We just want to allow to expose several async task methods. We want the tasks to get a lock without blocking
    – Kobi Hari
    Commented Feb 16, 2015 at 21:32
  • I wrote this up before i saw your comment... this example eliminates lock for readers, and it's pretty suitable for cache... anyway maybe what you are looking for is just SemaphoreSlim.WaitAsync?
    – AK_
    Commented Feb 16, 2015 at 21:47
  • Please see updates to question
    – Kobi Hari
    Commented Feb 17, 2015 at 13:24
0

If you expected more reads than writes a generally safe bet is ReaderWriterLockSlim which allows concurrent threads to read data, but only one thread may be a writer at a time.

You did mention concurrent lists and dictionaries, is there a reason that ConcurrentDictionary<,> doesn't work for this situation? I have found it to be quite performant and far less cumbersome to deal with in most situations.

For some performance critical solutions Interlocked.CompareExcgange works quite well, but this depends on your data that needs to be synchronized, CompareExchange works well with numeric types, but I would avoid reference based variables.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.