多个等待来自FIFO顺序中的单个线程恢复到同一个任务?

时间:2016-05-26 02:53:02

标签: c#

假设从单个线程多次创建Taskawait。恢复订单是FIFO吗?

简单示例: Debug.Assert()真的是不变的吗?

Task _longRunningTask;

async void ButtonStartSomething_Click()
{
    // Wait for any previous runs to complete before starting the next
    if (_longRunningTask != null) await _longRunningTask;

    // Check our invariant
    Debug.Assert(_longRunningTask == null, "This assumes awaits resume in FIFO order");

    // Initialize
    _longRunningTask = Task.Delay(10000);

    // Yield and wait for completion
    await _longRunningTask;

    // Clean up
    _longRunningTask = null;
}
为了简单起见,

Initialize Clean up 保持最低限度,但一般的想法是前一个 Clean up 运行之前,> Initialize 必须完整

3 个答案:

答案 0 :(得分:2)

执行顺序是预定义的,但如果从多个线程同时调用ButtonStartSomething_Click(),则_longRunningTask变量存在潜在的竞争条件(不太可能)。

或者,您可以使用队列显式计划任务。作为奖励,也可以使用非异步方法安排工作:

void ButtonStartSomething_Click()
{

        _scheduler.Add(async() =>
        {
              // Do something
              await Task.Delay(10000);
              // Do something else
        });
}

Scheduler _scheduler;  




class Scheduler
{
    public Scheduler()
    {
        _queue = new ConcurrentQueue<Func<Task>>();
        _state = STATE_IDLE;
    }


    public void Add(Func<Task> func) 
    {
       _queue.Enqueue(func);
       ScheduleIfNeeded();
    }

    public Task Completion
    {
        get
        {
            var t = _messageLoopTask;
            if (t != null)
            {
                return t;
            }
            else
            {
                return Task.FromResult<bool>(true);
            }
        }
    }

    void ScheduleIfNeeded()
    {

        if (_queue.IsEmpty) 
        {
            return;
        }

        if (Interlocked.CompareExchange(ref _state, STATE_RUNNING, STATE_IDLE) == STATE_IDLE)
        {
            _messageLoopTask = Task.Run(new Func<Task>(RunMessageLoop));
        }
    }

    async Task RunMessageLoop()
    {
        Func<Task> item;

        while (_queue.TryDequeue(out item))
        {
            await item();
        }

        var oldState = Interlocked.Exchange(ref _state, STATE_IDLE);
        System.Diagnostics.Debug.Assert(oldState == STATE_RUNNING);

        if (!_queue.IsEmpty)
        {
            ScheduleIfNeeded();
        }
    }


    volatile Task _messageLoopTask; 
    ConcurrentQueue<Func<Task>> _queue;
    static int _state;
    const int STATE_IDLE = 0;
    const int STATE_RUNNING = 1;

}

答案 1 :(得分:2)

简短的回答是:不,不能保证。

此外,you should not use ContinueWith;除了其他问题之外,它还有一个令人困惑的默认调度程序(我的博客上有更多细节)。您应该使用await代替:

private async void ButtonStartSomething_Click()
{
  // Wait for any previous runs to complete before starting the next
  if (_longRunningTask != null) await _longRunningTask;
  _longRunningTask = LongRunningTaskAsync();
  await _longRunningTask;
}

private async Task LongRunningTaskAsync()
{
  // Initialize
  await Task.Delay(10000);

  // Clean up
  _longRunningTask = null;
}

请注意,如果在任务仍在运行时可以多次单击该按钮,这仍然可能具有“有趣”的语义。

防止UI应用程序出现多重执行问题的标准方法是禁用按钮

private async void ButtonStartSomething_Click()
{
  ButtonStartSomething.Enabled = false;
  await LongRunningTaskAsync();
  ButtonStartSomething.Enabled = true;
}

private async Task LongRunningTaskAsync()
{
  // Initialize
  await Task.Delay(10000);
  // Clean up
}

这会强制您的用户进入一次一个操作队列。

答案 2 :(得分:1)

Task.ContinueWith()下找到答案。它似乎是:没有

假设 await is just Task.ContinueWith() under the hood TaskContinuationOptions.PreferFairness的文档内容为:

  

提示到TaskScheduler按照计划安排的顺序安排任务,以便更快安排的任务更有可能提前运行,以及稍后安排的任务更有可能在以后运行。

(粗体添加)

这表明不保证任何种类,固有的或其他的。

正确的方法

为了像我这样的人(OP),这里有一个更正确的方法。

根据Stephen Cleary的回答:

private async void ButtonStartSomething_Click()
{
    // Wait for any previous runs to complete before starting the next
    if (_longRunningTask != null) await _longRunningTask;

    // Initialize
    _longRunningTask = ((Func<Task>)(async () =>
    {
        await Task.Delay(10);

        // Clean up
        _longRunningTask = null;
    }))();

    // Yield and wait for completion
    await _longRunningTask;
}

Raymond Chen的评论建议:

private async void ButtonStartSomething_Click()
{
    // Wait for any previous runs to complete before starting the next
    if (_longRunningTask != null) await _longRunningTask;

    // Initialize
    _longRunningTask = Task.Delay(10000)
        .ContinueWith(task =>
        {
            // Clean up
            _longRunningTask = null;

        }, TaskContinuationOptions.OnlyOnRanToCompletion);

    // Yield and wait for completion
    await _longRunningTask;
}

Kirill Shlenskiy的评论建议:

readonly SemaphoreSlim _taskSemaphore = new SemaphoreSlim(1);

async void ButtonStartSomething_Click()
{
    // Wait for any previous runs to complete before starting the next
    await _taskSemaphore.WaitAsync();
    try
    {
        // Do some initialization here

        // Yield and wait for completion
        await Task.Delay(10000);

        // Do any clean up here
    }
    finally
    {
        _taskSemaphore.Release();
    }
}

(如果我弄乱了某些东西,请-1或评论。)

处理异常

使用continuation让我意识到一件事:如果await可以抛出异常,_longRunningTask在多个地方变得非常复杂。

如果我要使用continuation,看起来我需要通过处理continuation中的所有异常来完成它。

_longRunningTask = Task.Delay(10000)
    .ContinueWith(task =>
    {
        // Clean up
        _longRunningTask = null;

    }, TaskContinuationOptions.OnlyOnRanToCompletion);
    .ContinueWith(task =>
    {
        // Consume or handle exceptions here

    }, TaskContinuationOptions.OnlyOnFaulted);

// Yield and wait for completion
await _longRunningTask;

如果我使用SemaphoreSlim,我可以在try-catch中执行相同的操作,并添加了直接从ButtonStartSomething_Click中冒出异常的选项。