假设从单个线程多次创建Task
并await
。恢复订单是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
必须完整。
答案 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
中冒出异常的选项。