2
\$\begingroup\$

Edit: Use cases for this class - I needed this class in the case where I had processes I needed to execute (in the form of tasks - they were all asynchronous) that I needed to issue to a UI control when certain events occurred. However, these events are something I dont control and can sometimes occur in close succession. This class is used to make sure only the last event to have issued the task will "win" and run the task only once.

This class lets you run multiple tasks serially. If a task is already running (Task1), the next task (Task2) is "queued". However, if, before Task1 completes, Task3 is executed, it will replace Task2 and will be executed next.

The class is NOT thread safe.

/// <summary>
/// Lets you run multiple tasks serially. If a task is already running (Task1), the next task (Task2) is "queued". However, if,
/// before Task1 completes, Task3 is executed, it will replace Task2 and will be executed next.
/// THe class is NOT thread safe.
/// </summary>
public class LastTaskWins
{
    private object m_pendingToken;
    private Task m_currentTask;


    public async Task<bool> TryRunAsync(Func<Task> taskFunc)
    {
        bool result = true;

        // If there is no task running, just run the one queued.
        if (m_currentTask == null)
        {
            await ExecuteTaskNowAsync(taskFunc);
        }
        else
        {
            // Save a token so we know if we can run.
            object token = m_pendingToken = new object();

            // Make sure the current task completes.
            await m_currentTask;

            // Check to see if a new task has been "queued".
            result = token == m_pendingToken;
            if (result)
            {
                // If we can run (we own the last-queued task token), run the new task.
                await ExecuteTaskNowAsync(taskFunc);
            }

        }

        return result;
    }

    /// <summary>
    /// Helper method. Will run the relevant task, maintaining class state.
    /// </summary>
    private async Task ExecuteTaskNowAsync(Func<Task> taskFunc)
    {
        m_currentTask = taskFunc();
        await m_currentTask;
        m_currentTask = null;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Please do not just say: "Here's the code." and actually explain what your code does. \$\endgroup\$ – TheCoffeeCup Feb 5 '15 at 20:59
  • \$\begingroup\$ More than willing to do that - but I added comments exactly for that - I will copy over the header comments which explain the matter. Thanks. \$\endgroup\$ – Shahar Prish Feb 6 '15 at 6:14
  • 1
    \$\begingroup\$ Please describe the use cases for this class. I assume you have the Func<Task> parameter so that you call it only in case when the task should be executed, am I correct? \$\endgroup\$ – almaz Feb 6 '15 at 14:28
  • \$\begingroup\$ Added a description. And yes - because I dont want the task to just start running, I need a Func to issue it. \$\endgroup\$ – Shahar Prish Feb 7 '15 at 6:01
  • \$\begingroup\$ @ShaharPrish your code will always execute the task and will always return true because nobody else is changing m_pendingToken. \$\endgroup\$ – Bruno Costa Feb 7 '15 at 8:23
1
\$\begingroup\$

I suggest to make m_currentTask non-nullable by replacing usages of null with a completed task. It will simplify the code as you can always await on this field, and assume it is the "current" task running.

You would need to introduce locking in order to synchronize access to the continuation task, and ensure that the taskFunc is only called when it was a winning bid. Also, you would need a double-checked locking to ensure that your task is the winner:

public class LastTaskWins
{
    private readonly object m_pendingToken = new object();
    private Task m_currentTask = Task.FromResult(true); //It will always have a non-null value that you can await on
    private Func<Task> m_nextTaskFunc;


    public async Task<bool> TryRunAsync(Func<Task> taskFunc)
    {
        lock (m_pendingToken)
        {
            //No need in double-checked locking as the last wins
            m_nextTaskFunc = taskFunc;
        }

        // Current task is always present, and may already be completed (in which case await would run synchronously).
        await m_currentTask;

        Task currentTask;

        //Using double-checked locking to make sure we don't have concurrency issues around continuation task, and we only call taskFunc when going to await on it
        if (m_nextTaskFunc != taskFunc)
            return false;

        lock (m_pendingToken)
        {
            if (m_nextTaskFunc != taskFunc)
                return false;

            currentTask = m_currentTask = taskFunc();
            m_nextTaskFunc = null; //just to clean up the reference, as may represent a memory leak otherwise
        }

        await currentTask;
        return true;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Thanks for the suggestion... Question: Do you really think that adding locking to avoid the use of null here is worth it? The locking is localized and fairly simple, so it's really not a big deal, but when choosing null handling (again, all localized) vs locking, I think I would tend to choose null.. What do you think? \$\endgroup\$ – Shahar Prish Feb 18 '15 at 6:13
  • \$\begingroup\$ locking is done in order not to allow 2 different threads to modify m_nextTaskFunc at the same time. There is a lock-free solution as well, but it would require additional TaskCompletionSource and a loop to make sure this thread is the "winner", and to evaluate taskFunc only in case of winning the race. \$\endgroup\$ – almaz Feb 19 '15 at 6:54
  • \$\begingroup\$ Gotcha. The class is designed to not be thread safe. But that's definetly one way of going if I decide I need it to be thread safe. Thanks. \$\endgroup\$ – Shahar Prish Feb 19 '15 at 8:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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