目录
- 线程池线程更新UI
- 通过任务调度器
- 通过线程处理模型
- 线程池线程更新UI例子
- 小结
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方式更适合比较简单、基本不会出现异常、耗时较短的情况下使用