Task相关知识整理

  • 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 做了什么?

在这里插入图片描述
执行计算代码
执行UI更改代码

控制权的流转

暂停后续代码的执行,将控制权交还给调用者,在异步操作完成时继续执行后续代码。
控制权流转

文档对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;

    }

状态机内部执行过程

  1. 根据原代码await xxx 获取awaiter
  2. 根据awaiter判断异步方法是否完成,如果未完成,执行step3释放当前线程,如果完成执行step4拿到异步方法的返回值
  3. 将状态机的state置为awaiting,当前awaiter完成后调度statemachine的MoveNext;
  4. 拿到返回值。
 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完成时,如何继续执行异步方法的后续代码呢?
要引入一个新的概念,上下文
处理过程:

  1. 等待未完成的Task时,会捕获当前“上下文”
  2. 当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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值