HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

"Singleton" task running - using Tasks/await - peer review/challenge

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
challengeawaitpeerrunningtaskssingletonusingreviewtask

Problem

My app saves data to a file - I obviously don't want the app to write to the file from multiple locations on disk. What I want is a generic "utility" class that will know how to run a piece of code (let's call it SingletonTask) with the following rules:

  • Only one SingletonTask will run at any given moment in the system - EDIT: note that this singleton task may be asynchronous in nature and may require execution on the UI thread.



  • When multiple calls are made to run the SingletonTask, the first one will immediately run the SingletonTask and every subsequent call that happens before SingletonTask runs will "queue up".



  • All calls that are "queued up" during the run of SingletonTask will cause only a single new SingletonTask to execute (and all of them will complete when that one SingletonTask completes)



  • Obviously, if during the SingletonTask executed in (3), more calls come in, they will follow rule (2) and so on.



I could not find anything like this in .NET (looking for something that works on Win8) - but maybe I don't know what the correct keywords are.

In any case - here's my attempt at doing this.. I have a utility class called SigletonTask you inherit from. That class has a protected abstract method called RunProcessAsync(). The method users call is RunAsync() and it behaves as described in the above rules.

I tested this and it seems to work, however, there is an issue with it: It's currently limiting usage to from within the UI thread. This makes the solution simpler, but means you cannot use it from arbitrary threads. I looked into locking inside the method, but could not come up with something elegant that does not deadlock.

My questions are:

  • Can you see any issues with the code?



  • Can you make the code better?



Here's the SingletonTask class:

(EDIT: This is a new version. Old version at the bottom.)

(Also - it's based on cold tasks)

```
public abstract class SingletonTask
{
private Task m_runningTask = null;
private Task m_nextTa

Solution

I made something like this before but I don't claim that it's better.

Although it's much simpler:

public sealed class SingleTask
{
    private Task _Task;
    private readonly object _Lock = new object();

    // You can overload this with TaskCreationOptions, TaskScheduler etc.
    public SingleTask() { _Task = Task.FromResult(null); }

    // You can overload Queue methods with TaskContinuationOptions,
    // TaskScheduler, CancellationToken etc.
    public Task Queue(Action action)
    {
        lock (_Lock)
            return _Task = _Task.ContinueWith(t => action.Invoke());
    }
    public Task Queue(Func function)
    {
        lock (_Lock)
        {
            _Task = _Task.ContinueWith(t => function.Invoke());
            return _Task as Task;
        }
    }
}


You can use ContinueWith methods for queuing actions so I don't think you need a custom approach here. Here the Queue methods also return task objects so you can wait them individually.

You can make this class static if you want it to behave like a singleton.

But I can't see a reason for doing so.

As for your code, I don't see any issues with it.

Edit (Usage)

Let's say you have these methods you want to run asynchronously:

private void LongInitialization() { /* Some operation */ }
private int LongCalculation() { /* Some operation */ }


You can:

// Creates a completed task.
var single = new SingleTask();

// Starts initialization and returns its task.
Task initializeTask = single.Queue(LongInitialization);

// Queues LongCalculation to run after LongInitialization is completed.
Task calculateTask = task.Queue(LongCalculation);


In these samples however, you can't interact with UI controls because you can't access a control from a thread other than the one the control was created on. So if you have a method like this:

private void UpdateText() { txtStatus.Text = GetStatusFromDB(); }


...which interacts with a TextBox, and if you call single.Queue(UpdateText), you'll see an exception is thrown with the message: "Cross-thread operation not valid".

What you can do here to prevent this, is providing an overload to the SingleTask's Queue(Action) method that takes a TaskScheduler as a parameter:

public Task Queue(Action action, TaskScheduler scheduler)
{
    lock (_Lock)
        return _Task = _Task.ContinueWith(t => action.Invoke(), scheduler);
}


Then you can call it like this:

single.Queue(UpdateText, TaskScheduler.FromCurrentSynchronizationContext());


By calling Queue like this on a UI thread you say "Hey! Here is the UI's synchronization context, call its Post method so it won't break my program".

You also can provide overloads to Queue so it can take TaskContinuationOptions (if you want to specify LongRunning and make it run on a different thread, etc.) and/or CancellationToken parameters (if you want to provide cancellation). For example:

public Task Queue(Action action,
    TaskScheduler scheduler,
    TaskContinuationOptions options,
    CancellationToken token)
{
    lock (_Lock)
        return _Task = _Task
            .ContinueWith(t => action.Invoke(token), token, options, scheduler);
}


You can share the same SingleTask insance anywhere you want to do things asynchronously but in an order so, I hope this will satisfy your goals. Again, if you want to have only one instance of this, you can always make this class static.

Code Snippets

public sealed class SingleTask
{
    private Task _Task;
    private readonly object _Lock = new object();

    // You can overload this with TaskCreationOptions, TaskScheduler etc.
    public SingleTask() { _Task = Task<object>.FromResult(null); }

    // You can overload Queue methods with TaskContinuationOptions,
    // TaskScheduler, CancellationToken etc.
    public Task Queue(Action action)
    {
        lock (_Lock)
            return _Task = _Task.ContinueWith(t => action.Invoke());
    }
    public Task<T> Queue<T>(Func<T> function)
    {
        lock (_Lock)
        {
            _Task = _Task.ContinueWith<T>(t => function.Invoke());
            return _Task as Task<T>;
        }
    }
}
private void LongInitialization() { /* Some operation */ }
private int LongCalculation() { /* Some operation */ }
// Creates a completed task.
var single = new SingleTask();

// Starts initialization and returns its task.
Task initializeTask = single.Queue(LongInitialization);

// Queues LongCalculation to run after LongInitialization is completed.
Task<int> calculateTask = task.Queue(LongCalculation);
private void UpdateText() { txtStatus.Text = GetStatusFromDB(); }
public Task Queue(Action action, TaskScheduler scheduler)
{
    lock (_Lock)
        return _Task = _Task.ContinueWith(t => action.Invoke(), scheduler);
}

Context

StackExchange Code Review Q#16150, answer score: 7

Revisions (0)

No revisions yet.