《CLR via C#》读书笔记-异步编程(五)

目录

  1. 线程池线程更新UI
    1. 通过任务调度器
    2. 通过线程处理模型
  2. 线程池线程更新UI例子
  3. 小结

1 线程池线程更新UI
线程池中线程执行的线程完成后,有更新界面UI空间的需求。但是线程池中的线程无法更新UI,目前有两种方式解决这个问题
1.1 通过任务调度器
若程序中使用的是Task,则可以通过设定task的任务调度器,从而获取修改UI。具体的原理如下:
任务调度器的作用就是调度,并同时公开相关的信息(对Visual Studio调试器)。FCL提供了两个派生自TaskScheduler的类:线程池任务调度器和同步上下文任务调度器(在《CLR via C#》读书笔记-.NET多线程(五) 提到了详细的说明)。其中同步上下文任务调度器的作用就是将任务放置在GUI线程的队列中,这样任务就可以更新UI了。下面用《CLR via C#》的例子说明:

internal sealed class MyFrom:Form
{
    //Form的构造器,初始化标题等内容
    public MyFrom(){
        this.text="同步上下文任务调度器demo";
        visible=true;
        width=400;
        height=400;
    }

    //通过TaskScheduler的静态方法FromCurrentSynchronizationContext()获得当前Form的“同步上下文任务调度器”
    private readonly TaskScheduler m_syncContextTaskScheduler=
        TaskScheduler.FromCurrentSynchronizationContext();

    private CancellationTokeSource cts;

    //重写鼠标的点击方法
    protected override void OnMouseClick(MouseEventArgs e){
        if(cts!=null){
            cts.cancel();
            cts=null;
        }else{
            text="操作开始";
            cts = new CancellationTokeSource();

            //下面的这个task使用线程池的任务调度器,也就是默认的任务调度器  
            var task = new task(()=>Sum(cts.Token,2000),cts.Token);

            task.ContinueWith(
                t=>Text="结果为"+t.Result,
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnRanToCompletion,
                m_syncContextTaskScheduler
            ); 
            //通过最后一个参数,将任务分配给同步上下文任务调度器
            //这样,任务调度器就将任务放置于GUI线程队列中
            //GUI将更新界面的相关内容

            task.ContinueWith(
                t=>Text="操作被取消",
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnCanceled,
                m_syncContextTaskScheduler
            );

            task.ContinueWith(
                t=>Text="操作失败",
                CancellationToken.None,
                TaskContinuationOptions.OnlyOnFaulted,
                m_syncContextTaskScheduler
            );
        }//else结束
        base.OnMouseClick(e);
    }
}

1.2 通过线程处理模型
每个应用程序会有相应线程处理模型。类似于定义哪些线程在何时该做何种事情。
GUI应用程序(winform、sliverlight、wpf)引入了这样一种模型,在这种模型中,创建窗口的线程是唯一能更新窗口的线程。因此,按照这种说法,点击按钮、键盘输入,所有的输入都不会更新到GUI上。
控制台、ASP.NET Web窗体、XML Web服务应用程序的线程模型就是没有模型。任何线程想干啥就干啥,没啥限制。但是存在一个问题,用户发送请求后会将语言文化、身份标示等信息传至服务器,而执行具体操作的线程就会获得这些信息,但是若线程执行异步操作,需要额外的新线程协助完成任务时,这些语言文化、身份标识等信息并不会流向新的线程。
为了达到或实现“点击按钮或键盘输入后,GUI会做相应的变化”以及“新的线程也能得带请求中携带的语言文化、身份标识等信息”功能,FCL命名了一个System.Threading.SynchronizationContext基类,它解决了所有的问题。其定义如下:

public class SynchronizationContext{
    public static SynchronizationContext Current{get;}
    public virtual void Post(SendOrPostCallBack d,Object state);
    public virtual void Send(SendOrPostCallBack d,Object state);
}
//其中SendOrPostCallBack委托的定义如下
public delegate void SendOrPostCallBack(Object state);

其具体原理如下:GUI线程会与一个SynchronizationContext对象相关联,而这个“同步上下文”对象可以通过其Post或者是Send方法更新GUI线程的UI。具体的步骤如下:
1、声明一个变量。然后使用SynchronizationContext.Current属性获取GUI相关联的SynchronizationContext对象,将其保存在变量中
2、当线程池的线程完成操作之后,调用变量(第一步里声明的)的Post或者是Send方法。使GUI线程更新UI
这样就可以是GUI线程更新UI了。
在SynchronizationContext定义中,其有两个方法:Post和Send,前者是异步方法,后者是同步方法。即,线程池的线程完成操作,若调用Post方法,则线程池线程将任务放置在GUI线程队列后立刻返回;而send方法则是放置于GUI队列后一直等待UI更新后才返回。一般情况下尽量选择使用Post方法,避免因同步方法造成线程阻塞,从而线程池创建新的线程。
2.1 对于ASP.NET Web窗体和XML Web服务应用程序而言,是相同的。因客户请求而启动的线程与一个SynchronizationContext对象相关联。可以通过Current属性获取该对象并保存。当处理当前客户的请求需要额外的线程(新线程)协助处理时,在新线程中调用SynchronizationContext对象的Post方法,获取用户的语言文化、身份标识等信息
2.2 对于控制台程序,因为没有和线程关联的SynchronizationContext对象,也无需更新UI或者是或者请求的语言文化、身份标识等信息。
2 线程池线程更新UI例子
在《CLR via C#》中对SynchronizationContext操作的小方法,非常不错,具体如下:

private static AsyncCallback SyncContextCallback(AsyncCallback callback){
    //获取调用线程的SynchronizationContext对象
    SynchronizationContext sc=SynchronizationContext.Current;

    if(sc==null) return callback;

    return asyncResult=>sc.Post(result=>callback((IAsyncResult)result),asyncResult);
}

//   AsyncCallback的委托定义如下
//   public delegate void AsyncCallback(IAsyncResult ar)

作者的例子写的有点简洁,所以对该方法进行“分拆”,就变成这样:

private static AsyncCallback SyncContextCallback(AsyncCallback callback)
{
    //获取同步上下文
    System.Threading.SynchronizationContext sc = System.Threading.SynchronizationContext.Current;

    if (sc==null) return callback;

    //定义各一个SendOrPostCallback的委托,方便Post方法调用
    System.Threading.SendOrPostCallback sp = result => callback((IAsyncResult)result);

    //定义一个AsyncCallback委托,用于返回
    AsyncCallback ac = asyncResult =>
            {
                sc.Post(sp, asyncResult);
            };

     return ac;
}

理解了作者的方法的具体内容之后,就可以使用房作者提供的方法。
举例:使用APM编程方式,修改winform的text属性值

internal seald class MyWindowForm:Form{
    public MyWindowForm(){
        Text="点击窗体触发事件";
        Width=400;
        Height=100;
    }

    protected override void OnMouseClick(MouseEventArgs e){
        //GUI线程发起异步线程
        Text="GUI线程发起异步线程";
        var webRequest=WebRequest.Create(@"https://www.baidu.com/");
        webRequest.BeginGetResponse(SyncContextCallback(ProcessWebResponse),webRequest);
    }

    private void ProcessWebResponse(IAsyncResult result){
        var webRequest = (IAsyncResult)result.AsyncState;
        using(var webResponse=webRequest.EndGetResponse(result)){
            Text="获取的内容长度"+webResponse.ContentLength;
        }
    }
}

3.小结
线程池线程更新UI的这两种方式,理解如下:
1、Task的适合耗时、长时间、可能需要捕获异常的操作。例如,执行长时间的时间查询,查询后将结果放在datagridview中。
2、APM方式更适合比较简单、基本不会出现异常、耗时较短的情况下使用

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值