- bug整理
- Task原理
Task.Factory.New 部分Task未执行
问题描述
用Task.Factory.New 创建多个Task用于上传数据,出现部分Task未执行的状况。
var task = Task.Factory.StartNew(async () =>
{
try
{
var r = await modelContainer.UploadTextureAsync(tex);
if (r.ResultType != ResultType.Ok)
{
texQueue.Enqueue(tex);
}
}
finally { }
});
tasks.Add(task);
换成Task.Run()后,bug修复
原理探查
查看源码可知:
Task.Run(A); (代码a)
相当于
Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);(代码b)
Task.Factory.StartNew(A);(代码c)
相当于
Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current); (代码d)
但是,使用 代码b 后,部分Task未执行的状况偶尔会复现!
var task = Task.Factory.StartNew(async () =>
{
try
{
var r = await modelContainer.UploadTextureAsync(tex);
if (r.ResultType != ResultType.Ok)
{
texQueue.Enqueue(tex);
}
}
finally { }
}, default(CancellationToken), TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
tasks.Add(task);
查阅官方文档,发现,文档给的例子没有用async关键字,原来:
1.当上下文是异步代码时_,_Task.Factory.StartNew不识别async委托,该任务仅表示委托的开始,async 是在 .net framework 4.5 之后出现的。
增加进度条【WinForm】
Task.Delay的使用
主线程:创建窗口,更新提示信息
后台线程:处理堵塞操作,读取模型数据。
private async void button1_Click(object sender, EventArgs e)
{
//UI 线程
using (var dlgProgress = new DlgUploadProgress("导出模型中","开始读取数据")){
dlgProgress.show();
await ExecuteMethodAsync();//创建Task的Call Thread是UI线程
}
}
private async Task ExecuteMethodAsync()
{
//后台线程
for (int percentComplete = 0; percentComplete < 1000; percentComplete++)
{
dlgProgress.ReportProgress(15, $"读取{i}-{_oaFragmentDic.Count}图元数据");
await Task.Delay(10);//暂停Task,
}
}
public void ReportProgress(int progress, string msg)
{
BeginInvoke(new Action(() => {
progressBar.Style = ProgressBarStyle.Marquee;
label.Text = $"正在 {msg}...";
}));
}
原理
UI线程与子线程
Update GUI In Async Mehtod
只能通过创建控件的线程来修改GUI,原因是防止其他工作线程与UI线程同时更新界面,造成画面混乱和死锁现象。所以ISynchronizeInvoke诞生了
ISynchronizeInvoke
一个“源”线程将一个委托封送到“目标”线程的工作队列。通过InvokeRequired属性来确定当前代码是否在目标线程上运行。
public interface ISynchronizeInvoke
{
[HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
IAsyncResult BeginInvoke(Delegate method, object[] args);
object EndInvoke(IAsyncResult result);
object Invoke(Delegate method, object[] args);
bool InvokeRequired { get; }
}
但是,ISynchronizeInvoke 不太适合 ASP.NET 异步页面体系结构,因此诞生了SynchronizationContext 。
SynchronizationContext
- 可以使工作单元(而不是特定线程)列入上下文。
- 每个线程都有“当前”上下文,线程上下文实例可以跟多个其他线程共享
- 保持未完成操作的计数。
- 捕获到当前syncCtx时,计数增加;
- 捕获到的syncCtx用于将一个完成通知排到上下文时。 计数减少
// The important aspects of the SynchronizationContext APIclass SynchronizationContext
{
// Dispatch work to the context.
void Post(..); // (asynchronously)
void Send(..); // (synchronously)
// Keep track of the number of asynchronous operations.
void OperationStarted();
void OperationCompleted();
// Each thread has a current context.
// If "Current" is null, then the thread's current context is
// "new SynchronizationContext()", by convention.
static SynchronizationContext Current { get; }
static void SetSynchronizationContext(SynchronizationContext);
}
WindowsFormsSynchronizationContext
winform会创建并安装一个 WindowsFormsSynchronizationContext 作为创建 UI 控件的任意线程的当前上下文。
winformSyncCtx的上下文是一个单独的UI线程,所有被封送到此UI线程的委托,一次只能执行一个。
DispatcherSynchronizationContext
用于wpf
Default (ThreadPool) SynchronizationContext
如果一个线程的当前 SynchronizationContext 为 null,那么它隐式具有一个默认 SynchronizationContext。其异步委托列队到 ThreadPool
应用于 ThreadPool 线程和显式子线程(Thread类的实例)
UI 应用程序通常有两个同步上下文:包含 UI 线程的 UI SynchronizationContext 和包含 ThreadPool 线程的默认 SynchronizationContext。
参考文档
Control.BeginInvoke与Invoke
Windows消息机制
- UI线程维护一个消息队列,当UI线程获取新消息,对窗体上的窗口过程进行调用,执行GUI更新相关操作
- 是Windows平台上的线程或进程间通信机制之一,提供API使一个线程可以向另一个线程的消息队列发送消息。
Windows GUI程序的消息循环:windows程序使用GetMessage获取消息,从消息队列中通过while循环获取消息,当消息队列为空时,GetMessage阻塞,while循环停止。(所以在UI线程执行耗时计算,会使界面卡顿。)
- SendMessage():可以通过窗口句柄把消息发送到指定窗口的主线程消息队列。该方法阻塞,OS会确保消息确实发送到目的地,且该消息被处理后,方法才返回,返还控制权给调用者。
- PostMessage():发送消息到窗口的消息队列,调用者调用该方法后立刻返回,不会阻塞。
- 有些API可以通过线程句柄把消息发送到该线程的消息队列中。
BeginInvoke与Invoke
**Control.BeginInvoke:**在创建控件的基础句柄所在线程上异步执行指定委托
**Control.Invoke:**在拥有此控件的基础窗口句柄所在线程上执行指定委托
二者在内部的实现上都是使用PostMessage方法。其中Invoke方法的同步阻塞是使用WaitHandle机制等待异步操作的完成。
在async方法中Update GUI
private async void Button_Clicked(object sender, EventArgs e){
//UI context
await Test();
}
async Task TestInvoke(){
dialog.Show();
//UI Context
Control.Invoke(new Action(()=>{ this.text="11"});
/*
如果没有此耗时任务,变化太快 text无法更新
可以增加一个可等待任务,让界面变化在人可感知范围内
await Task.Yield();
*/
doSomething1();
Control.Invoke(new Action(()=>{ this.text="22"});
await Task.Yield();
//why "22" not show
}
async Task TestBeginInvoke(){
dialog.Show();
//异步,不会等待消息是否处理
Control.BeginInvoke(new Action(()=>{ this.text="11"});
/*
可以增加一个可等待任务
*/
await Task.Yield();
}
前提条件:一直都在主线程执行
await 做了什么?
控制权的流转
暂停后续代码的执行,将控制权交还给调用者,在异步操作完成时继续执行后续代码。
文档对await的作用做出了解释,但
- 如何暂停后续代码?
- 如何继续执行后续代码?
需要了解一个概念:状态机。
状态机
What
- 状态机通过事件驱动方式对工作流建模,状态State、事件Event、动作Action、转换Transition构成了状态机的逻辑。
IAsyncStateMachine
接口,通过MoveNext
和SetStateMachine
处理相关业务
public interface IAsyncStateMachine
{
//
// Summary:
// Moves the state machine to its next state.
void MoveNext();
//
// Summary:
// Configures the state machine with a heap-allocated replica.
//
// Parameters:
// stateMachine:
// The heap-allocated replica.
void SetStateMachine(IAsyncStateMachine stateMachine);
}
- State表示
- Completed:-2(结束状态)
- Created: -1(默认状态)
- Awaiting: 0,1,2… ( 非负整数):当有多个await的时候,每个await都会改变状态机的状态,比如 改为 0,1,2,3,4 等等, 分别表示 代码中await xxx 这句话执行完成。
它以状态-1启动,每当await一个未完成的task时,其状态将转换为一个非负整数,直到task完成才被再次转换为-1,如此反复直到执行完所有代码(即执行到source code中的return)才以状态-2结束。
How
async,标记xxxMethod
是异步方法,编译器通过async标记,将函数体改为状态机函数体
:::warning
Q:什么情况下会编译成状态机函数体?
A:1)async标记的函数体,2)计算密集型异步函数的调用者
:::
await,为了实现状态机的一个状态,函数体中每当有一个await,产生一个 TaskAwaiter awaiter,改变状态机的状态。
编译器执行过程
- 根据async标记,将async Caller()函数体编译改为状态机函数体
- 根据await F1() 创建F1_StateMachine();设置初始state=-1。
- 启动状态机,stateMachine.builder.Start
- return stateMachine.builder.Task
[AsyncStateMachine(typeof(<F3Async>d__3))]
[DebuggerStepThrough]
public static Task<int> F3Async()
{
<F3Async>d__3 stateMachine = new <F3Async>d__3();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder<int> <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
状态机内部执行过程
- 根据原代码await xxx 获取awaiter
- 根据awaiter判断异步方法是否完成,如果未完成,执行step3释放当前线程,如果完成执行step4拿到异步方法的返回值
- 将状态机的state置为awaiting,当前awaiter完成后调度statemachine的MoveNext;
- 拿到返回值。
private sealed class <F3Async>d__3:IAsyncStateMachine
{
/*
属性,字段声明
state;
builder;
awaiter;
<>s_1;//存放一个task的结果
*/
void SetStateMachine(IAsyncStateMachine stateMachine)
{
// Obsolete contract.
}
void IAsyncStateMachine.MoveNext(){
TaskAwaiter awaiter1;
if(this.state!=State.Awaiting){
awaiter1=XXXX.GetAwaiter();//await xxxx 原码编译
if(!awaiter1.IsCompleted){
this.state = 0;
var stateMachine = this;
this.awaiter=awaiter1;
/*
释放当前线程,将控制权返还给caller
== awaiter1.onCompleted(() => this.MoveNext())
== task.ContinueWith(()=> this.MoveNext())
*/
this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref stateMachine);
return;
}
/*
多个await
继续编译第二个task,执行step2.
多个await,通过goto 一步一步跳转,按顺序执行。
*/
}else{
//表示任务执行结束
awaiter = this.awaiter
this.awaiter = new TaskAwaiter();
this.state = State.Created;
}
<>s_1=waiter.GetResult();
result = <>s_1;
// RelatedTo._dictionaryCache.Add(Key, result);
}
}
参考文档
C# Async/Await原理剖析_高语越的博客-程序员ITS203_async await原理 - 程序员ITS203
C# Async/Await原理剖析_高语越的博客-程序员ITS203_async await原理 - 程序员ITS203
await 处理上下文
根据状态机的执行过程了解到,异步方法不会阻塞的原因是:awaiter判断任务出于未完成状态,就会释放当前线程,返还控制权给caller,当Task完成时,如何继续执行异步方法的后续代码呢?
要引入一个新的概念,上下文
处理过程:
- 等待未完成的Task时,会捕获当前“上下文”
- 当Task完成后,在捕获的上下文中执行async方法剩余的部分。
SynchronizationContext(td)
MSDN:提供在各种同步模型中传播同步上下文的基本功能,实现线程之间通讯。
What
线程运行的环境,定义线程如何响应消息的一组特征
[__DynamicallyInvokable]
public virtual void Send(SendOrPostCallback d, object state)
{
d(state);
}
[__DynamicallyInvokable]
public virtual void Post(SendOrPostCallback d, object state)
{
ThreadPool.QueueUserWorkItem(d.Invoke, state);
}
Send():源线程委托工作给目标线程,源线程阻塞,直到目标线程完成
创建:
只有UI线程一定有SyncCtx对象,UI线程在创建第一个控件时,把SyncCtx对象放入UI线程中。
其他线程可以自己new一个SyncCtx对象。
post方法:
base实现,异步调用,将接收的委托加入线程池队列去异步执行。
Send方法:
base实现,同步调用,在当前线程上调用委托。
eg
public void DoWork(Action worker, Action completion)
{
SynchronizationContext sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
worker();
}
finally
{
sc.Post(_ => completion(), null);
}
});
}
TaskScheduler(td)
ThreadPoolTaskScheduler,task默认调度机制,通过LongRunning判断将task委托到Thread中,还是ThreadPool中。
SynchronizationContextTaskScheduler,同步上下文的taskscheduler,把耗时工作丢给ThreadPool,更新UI操作分给UI线程。
TaskScheduler.FromCurrentSynchronizationContext()
ConfigureAwait
ConfigureAwait(false)的作用:
- 实现少量并行:某些异步代码可以与GUI线程并行,
- 避免死锁:当等待完成时,尝试在线程池上下文执行async方法的剩余部分。
async Task MyMethodAsync()
{
// Code here runs in the original context.
await Task.Delay(1000);
// Code here runs in the original context.
await Task.Delay(1000).ConfigureAwait(
continueOnCapturedContext: false);
// Code here runs without the original
// context (in this case, on the thread pool).
}
在需要使用当前上下文的代码中,不应使用ConfigureAwait,比如GUI应用程序
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Can't use ConfigureAwait here ...
await Task.Delay(1000);
}
finally
{
// Because we need the context here.
button1.Enabled = true;
}
}
SynchronizationContext.Post的重写
Winform派生自SynchronizationContext的类(简称pSyncCtx)重写了Post方法,内部执行Control.BeginInvoke(wpf是Dispatcher.BeginInvoke),调用该Post方法就会在该控件的UI线程上执行接收的委托,更新控件。
ConfigureAwait(continueOnCapturedContext: false)
影响捕获目标上下文或调度程序的逻辑。
创建一个委托,默认在捕获context,参数置为false后,不会在原始context排队,但可能存在所有task结束,继续执行此委托
如何将工作单元从工作线程传递到 UI 线程
_SynchronizationContext.Send()_将执行传递给主 UI 线程
_await_在执行异步任务(离开当前同步上下文)之前捕获当前同步上下文。异步任务返回后,它再次引用原始同步上下文(重新进入同步上下文)和其余的方法连续。
设置为UI线程的同步上下文
参考文档
StartNew is Dangerous
使用 Async和 Await 的任务异步编程 (TAP) 模型 (C#)
异步编程 - C#
Task
发展历程
- 1.0 Asynchronous Programming Model (APM)
- 2.0 Event-based Asynchronous Pattern (EAP)
- 3.5 Tread ThreadPool
- 4.0 任务并行库 (TPL) Task
- c#5.0 Task-based Asynchronous Pattern (TAP) Async和Await关键字 Task.Run
- c#7.0 async Task Main
- c#8.0 IAsyncEnumerable asynchronous-streams
并行编程
异步编程
参考文档:
https://www.cnblogs.com/huangxincheng/p/14754776.html