我有一个自定义的等待类型,问题是继续在不同的线程上继续进行,这会在WinForms / WPF / MVC / etc等UI中引起问题:
private MyAwaitable awaitable;
private async void buttonStart_Click(object sender, EventArgs e)
{
awaitable = new MyAwaitable(false);
progressBar1.Visible = true;
// A regular Task can marshal the execution back to the UI thread
// Here ConfigureAwait is not available and I don't know how to control the flow
var result = await awaitable;
// As a result, here comes the usual "Cross-thread operation not valid" exception
// A [Begin]Invoke could help but regular Tasks also can handle this situation
progressBar1.Visible = false;
}
private void buttonStop_Click(object sender, EventArgs e) => awaitable.Finish();
这是MyAwaitable
类:
public class MyAwaitable
{
private volatile bool finished;
public bool IsFinished => finished;
public MyAwaitable(bool finished) => this.finished = finished;
public void Finish() => finished = true;
public MyAwaiter GetAwaiter() => new MyAwaiter(this);
}
有问题的自定义侍者:
public class MyAwaiter : INotifyCompletion
{
private readonly MyAwaitable awaitable;
private readonly SynchronizationContext capturedContext = SynchronizationContext.Current;
public MyAwaiter(MyAwaitable awaitable) => this.awaitable = awaitable;
public bool IsCompleted => awaitable.IsFinished;
public int GetResult()
{
var wait = new SpinWait();
while (!awaitable.IsFinished)
wait.SpinOnce();
return new Random().Next();
}
public void OnCompleted(Action continuation)
{
// continuation(); // This would block the UI thread
// Task constructor + Start was suggested by the references I saw,
// Results with Task.Run/Task.Factory.StartNew are similar.
var task = new Task(continuation, TaskCreationOptions.LongRunning);
// If executed from a WinForms app, we have a WinFormsSyncContext here,
// which is promising, still, it does not solve the problem.
if (capturedContext != null)
capturedContext.Post(state => task.Start(), null);
else
task.Start();
}
}
我怀疑我的OnCompleted
实现不太正确。
我试图深入研究ConfiguredTaskAwaiter
方法返回的Task.ConfigureAwait(bool).GetAwaiter()
,发现黑魔法发生在SynchronizationContextAwaitTaskContinuation
类中,但这是一个内部类,还有很多其他内部使用的类型。有没有一种方法可以重构我的OnCompleted
实现以达到预期的效果?
更新:投票者注意:我知道我在OnCompleted
中做了不当的事情,这就是为什么要问这个问题。如果您对质量(或其他任何方面)有疑问,请发表评论并帮助我改善问题,以便我也可以帮助您更好地突出问题。谢谢。
注释2 :我知道我可以对TaskCompletionSource<TResult>
及其常规Task<TResult>
结果使用变通办法,但我想了解背景知识。 这是唯一的动机。纯粹的好奇心。
更新2 :我调查过的重要参考文献:
服务员的工作方式:
一些实现:
答案 0 :(得分:4)
OnCompleted
方法的MSDN解释是:
安排实例完成时调用的继续操作。
因此OnCompleted
的两个实现都不是“正确的”,因为如果awaiter
尚未完成,则awaitable
不应在该调用期间执行传递的委托,但是注册awaitable
完成后执行。
唯一不清楚的是,如果awaitable
在调用方法时已经完成,该方法应该做什么(尽管在这种情况下编译器生成的代码不会调用它)-忽略连续委托或执行。根据{{1}}的实现,应该晚些(执行)。
当然,该规则也有例外(因此,“正确” 一词)。例如,Task
特别地总是返回YieldAwaiter
来强制调用它的IsCompleted == false
方法,该方法立即在线程池上调度传递的委托。但是“通常”您不会那样做。
通常(与标准OnCompleted
实现一样),Task
将执行操作,提供结果,等待机制,并维护/执行延续。他们的awaitable
通常是awaiters
,持有对共享struct
的引用(必要时还有继续选项),并将委托awaitable
和GetResult
方法调用到共享的OnCompleted
中,并且专门用于awaitable
传递延续委托以及负责注册/执行它们的OnCompleted
内部方法的选项。 “可配置” awaitable
将仅保存共享的awaitable
和选项,然后将其传递给创建的awaitable
。
由于在您的示例中,等待和结果由awaiter
提供,所以awaiter
可以简单地提供完成事件:
awaitable
,然后public class MyAwaitable
{
private volatile bool finished;
public bool IsFinished => finished;
public event Action Finished;
public MyAwaitable(bool finished) => this.finished = finished;
public void Finish()
{
if (finished) return;
finished = true;
Finished?.Invoke();
}
public MyAwaiter GetAwaiter() => new MyAwaiter(this);
}
个订阅:
awaiter
调用public class MyAwaiter : INotifyCompletion
{
private readonly MyAwaitable awaitable;
private int result;
public MyAwaiter(MyAwaitable awaitable)
{
this.awaitable = awaitable;
if (IsCompleted)
SetResult();
}
public bool IsCompleted => awaitable.IsFinished;
public int GetResult()
{
if (!IsCompleted)
{
var wait = new SpinWait();
while (!IsCompleted)
wait.SpinOnce();
}
return result;
}
public void OnCompleted(Action continuation)
{
if (IsCompleted)
{
continuation();
return;
}
var capturedContext = SynchronizationContext.Current;
awaitable.Finished += () =>
{
SetResult();
if (capturedContext != null)
capturedContext.Post(_ => continuation(), null);
else
continuation();
};
}
private void SetResult()
{
result = new Random().Next();
}
}
时,首先检查是否完整。如果是,我们只执行传递的委托并返回。否则,我们将捕获同步上下文,订阅OnCompleted
完成事件,然后在该事件内部通过捕获的同步上下文或直接执行操作。
同样,在现实生活中,awaitable
应该执行真实的工作,提供结果并维护延续动作,而awaitable
s应该仅注册延续动作,最终抽象出延续执行策略-直接通过捕获的同步上下文,通过线程池等。
答案 1 :(得分:0)
注意:最初,我在@IvanStoev给出正确答案后将此问题作为总结放在问题的结尾(非常感谢您的启发)。现在,我将该部分提取为真实答案。
因此,根据Ivan的回答,这里是一个简短的摘要,其中包含缺少的部分,我认为应该在文档中。下面的示例还模仿了ConfigureAwait
的{{1}}行为。
1。测试应用
一个带有Task
和3个ProgressBar
控件的WinForms应用程序(也可以是其他单线程UI):一个按钮只是启动一个异步操作(和一个进度栏),其他按钮完成要么在UI线程中,要么在外部线程中。
Button
2。自定义等待类
与问题中的原始示例相反,此处的等待类本身包含延续,一旦执行完成,便会调用该延续。因此,侍应生可以要求安排继续执行的时间以供以后执行。
请注意,public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
progressBar.Style = ProgressBarStyle.Marquee;
progressBar.Visible = false;
}
private MyAwaitable awaitable;
private async void buttonStart_Click(object sender, EventArgs e)
{
awaitable = new MyAwaitable();
progressBar.Visible = true;
var result = await awaitable; //.ConfigureAwait(false); from foreign thread this throws an exception
progressBar.Visible = false;
MessageBox.Show(result.ToString());
}
private void buttonStopUIThread_Click(object sender, EventArgs e) =>
awaitable.Finish(new Random().Next());
private void buttonStopForeignThread_Click(object sender, EventArgs e) =>
Task.Run(() => awaitable.Finish(new Random().Next()));
}
和ConfigureAwait
基本相同-后者可以使用默认配置。
GetAwaiter
3。侍者
public class MyAwaitable
{
private volatile bool completed;
private volatile int result;
private Action continuation;
public bool IsCompleted => completed;
public int Result => RunToCompletionAndGetResult();
public MyAwaitable(int? result = null)
{
if (result.HasValue)
{
completed = true;
this.result = result.Value;
}
}
public void Finish(int result)
{
if (completed)
return;
completed = true;
this.result = result;
continuation?.Invoke();
}
public MyAwaiter GetAwaiter() => ConfigureAwait(true);
public MyAwaiter ConfigureAwait(bool captureContext)
=> new MyAwaiter(this, captureContext);
internal void ScheduleContinuation(Action action) => continuation += action;
internal int RunToCompletionAndGetResult()
{
var wait = new SpinWait();
while (!completed)
wait.SpinOnce();
return result;
}
}
现在不执行延续(不同于我研究的示例),而是通过调用OnCompleted
将其注册以供以后使用。
第二,请注意,现在等待者还有一个MyAwaitable.ScheduleContinuation
方法,它只会返回自身。 GetAwaiter
的用法需要使用它。
await myAwaitable.ConfigureAwait(bool)
答案 2 :(得分:-1)
这证明了延续在捕获的上下文中运行:
public class MyAwaitable
{
private volatile bool finished;
public bool IsFinished => finished;
public MyAwaitable(bool finished) => this.finished = finished;
public void Finish() => finished = true;
public MyAwaiter GetAwaiter() => new MyAwaiter(this);
}
public class MyAwaiter : INotifyCompletion
{
private readonly MyAwaitable awaitable;
private readonly SynchronizationContext capturedContext = SynchronizationContext.Current;
public MyAwaiter(MyAwaitable awaitable) => this.awaitable = awaitable;
public bool IsCompleted => awaitable.IsFinished;
public int GetResult()
{
SpinWait.SpinUntil(() => awaitable.IsFinished);
return new Random().Next();
}
public void OnCompleted(Action continuation)
{
if (capturedContext != null) capturedContext.Post(state => continuation(), null);
else continuation();
}
}
public class MySynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine("Posted to synchronization context");
d(state);
}
}
class Program
{
static async Task Main()
{
SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
var awaitable = new MyAwaitable(false);
var timer = new Timer(_ => awaitable.Finish(), null, 100, -1);
var result = await awaitable;
Console.WriteLine(result);
}
}
输出:
Posted to synchronization context
124762545
但是您没有将延续发布到同步上下文中。
您要发布调度在另一个线程上继续执行的时间。
计划在同步上下文上运行,但连续本身却没有。因此就是你的问题。
您可以阅读this来了解其工作原理。