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_nextTask = null;
private object m_lock = new object();
public SingletonTask()
{
}
public Task RunAsync()
{
return RunAsync(() => RunProcessAsync());
}
public async Task RunAsync(Func<Task> taskGetter)
{
Task task1 = null;
Task task2 = null;
ColdTask start1 = null;
ColdTask start2 = null;
lock (m_lock)
{
if (m_runningTask == null)
{
start1 = new ColdTask(taskGetter);
Task innerTask = start1.GetTask();
m_runningTask = innerTask.ContinueWith(x => { lock (m_lock) { m_runningTask = null; } });
task1 = m_runningTask;
}
else
{
task1 = m_runningTask;
if (m_nextTask == null)
{
start2 = new ColdTask(taskGetter);
Task innerTask = start2.GetTask();
m_nextTask = innerTask.ContinueWith(x => { lock (m_lock) { m_nextTask = null; } });
}
task2 = m_nextTask;
}
}
if (start1 != null)
{
start1.Start().RunWithoutWarning();
}
if (task1 != null)
{
await task1;
}
if (start2 != null)
{
start2.Start().RunWithoutWarning();
}
if (task2 != null)
{
await task2;
}
}
protected virtual Task RunProcessAsync()
{
return null;
}
}
And here's how you inherit from it:
class MySing : SingletonTask
{
private int m_id = 1000;
Action<string> log;
public MySing(Action<string> log, CoreDispatcher dispatcher) : base(dispatcher)
{
this.log = log;
}
protected override async Task RunProcessAsync()
{
int id = m_id++;
log("Before delay for " + id.ToString());
await Task.Delay(4000);
log("After delay for " + id.ToString());
}
}
My test app is fairly simple - there's a button that calls into an instance of MySing
and awaits the result, logging before awaiting and after awaiting:
private void Button_Click_1(object sender, RoutedEventArgs e)
{
int id = m_id++;
Log("Running with id=" + id.ToString());
await m_sing.RunAsync();
Log("Run returned with id=" + id.ToString());
}
Tap the button 3 times quickly, for example, and you should see:
Running with id=0
Before delay for 1000
runnjng with id=1
Running with id=2
After delay for 1000
Run returned with id=0
Before delay for 1001
After delay for 1001
Run returned with id=1
Run returned with id=2
(I hope asking such a question is kosher here)
------------------------- OLD -------------------------------
(This is the code for my original question, before the edit.)
public abstract class SingletonTask
{
private Task m_runningTask = null;
private Task m_nextTask = null;
private object m_lock = new object();
private CoreDispatcher m_dispatcher = null;
public SingletonTask(CoreDispatcher dispatcher)
{
m_dispatcher = dispatcher;
}
public async Task RunAsync()
{
if (!m_dispatcher.HasThreadAccess)
{
throw new UnauthorizedAccessException("Unauthorized thread access");
}
Task firstRun = null;
if (m_runningTask == null)
{
Task innerRun = RunProcessAsync();
m_runningTask = innerRun.ContinueWith(x => m_runningTask = null);
firstRun = innerRun;
}
else if (m_nextTask == null)
{
await m_runningTask;
if (m_runningTask == null)
{
Task innerRun = RunProcessAsync();
m_runningTask = innerRun.ContinueWith(x => m_runningTask = null);
}
firstRun = m_runningTask;
}
if (firstRun != null)
{
await firstRun;
}
}
protected abstract Task RunProcessAsync();
}