SynchronizationContext 是 C# 内置跨线程同步上下文的基类,可以实现线程间的同步。
例如,一般情况下子线程无法修改主线程的 UI,通常比较麻烦的做法就是自己采用轮询机制实现同步。
而该类则可很方便地处理该类型问题。
# 前言
官方文档地址:地址
说实话,官方文档的描述极少,大概就是说这个类是个基类,提供了一个通用的线程上下文的传递模型,有需要可以直接继承该类,并重载需要的方法,简化了多线程工作单元传递所需的工作。
不过在该文档中,还额外提供了一份 MSDN 的文章 。
MSDN 文章就比较详细些了。
原理就不多说了,上面 MSDN 文章有详细介绍,这种系统层级的东西,用就好了。
另外虽说称为基类,但是直接使用也是没问题的。
# 介绍
在 SynchronizationContext 中,有静态的 Current 变量,在主 UI 线程调用将返回一个 SynchronizationContext 类型的实例。
同时也提供了 SetSynchronizationContext (SynchronizationContext syncContext) 方法可覆盖该返回值,一般用于继承了 SynchronizationContext 并有自己额外处理的方式的派生类使用。
可以将 Current 当做一个单例,但是注意的是它属于每个线程独有,在 UI 主线程可以直接通过 SynchronizationContext.Current 获取到默认的实例,但新开的子线程默认始终为 Null。
注:即便使用 SetSynchronizationContext 在主线程设置之后,新开的子线程若不进行设置,依然会保持为 Null。
SynchronizationContext 类中,主要提供 Post、Send 两个方法进行线程回调使用:
public virtual void Post(SendOrPostCallback d, object state); | |
public virtual void Send(SendOrPostCallback d, object state); |
其中调用 Post 将会异步执行,调用 Send 则同步执行。 object state
为调用回调时,传入的参数。
- Post:在调用之后,立即执行当前线程后续代码。
- Send:在调用之后,等待回调执行完毕,才会继续执行当前线程后续代码。
测试如下:
Send
Task.Run(() => | |
{ | |
SynchronizationContext.SetSynchronizationContext(context); | |
SynchronizationContext.Current.Send((args) => | |
{ | |
Thread.Sleep(1000); | |
Console.WriteLine($"子线程调用UI!args:{args}"); | |
}, "参数X"); | |
Console.WriteLine("调用结束!"); | |
}); |
在该方法中,调用 Send,并在回调内休眠 1000 毫秒。
打印结果:
子线程调用UI!args:参数X
调用结束!
按照顺序执行,当子线程休眠时,后续代码也阻塞未执行,直到 Send 回调执行完毕。
Post
直接将上述代码中的 Send 改为 Post,结果如下:
调用结束!
子线程调用UI!args:参数X
即使去掉回调中的 1000 毫秒休眠,调用 Post,依然会是该结果。
因此可以表明:Post 可用于异步回调,Send 可用于等待回调执行,需要阻塞后续代码的情况。
# 使用
# 简单应用
直接使用的方法很简单,这里先来看 WPF 中的效果。
正常情况下,子线程无法操作 UI,若直接调用,会得到异常报错,例如如下代码:
Task.Run(() => | |
{ | |
TestBtn.Content = "子线程调用UI!"; | |
}); |
会得到如下报错:
在 WPF 中,虽然有其它方式可以避免报错并让其正常工作,例如:
Task.Run(() => | |
{ | |
TestBtn.Dispatcher.Invoke(() => | |
{ | |
TestBtn.Content = "子线程调用UI!"; | |
}); | |
}); |
不过这种方式是 WPF 自己的特性,如果想在其它地方使用,比如 Unity,可就没这种东西了。
此时便可使用 SynchronizationContext:
SynchronizationContext context = SynchronizationContext.Current; | |
Task.Run(() => | |
{ | |
SynchronizationContext.SetSynchronizationContext(context); | |
SynchronizationContext.Current.Send((args) => | |
{ | |
Console.WriteLine($"子线程调用UI!"); | |
TestBtn.Content = "子线程调用UI!"; | |
}, null); | |
Console.WriteLine("调用结束!"); | |
}); |
该份代码复制至 Unity,且调用 UI 替换为 Unity 的函数,亦可正常工作:
void Start() | |
{ | |
SynchronizationContext context = SynchronizationContext.Current; | |
Task.Run(() => | |
{ | |
SynchronizationContext.SetSynchronizationContext(context); | |
Console.WriteLine(SynchronizationContext.Current.GetHashCode()); | |
SynchronizationContext.Current.Send((args) => | |
{ | |
Console.WriteLine("子线程调用UI!"); | |
UnityEditor.EditorUtility.DisplayDialog("提示", "子线程的调用!", "OK"); | |
}, null); | |
Console.WriteLine("调用结束!"); | |
}); |
# 单例
一般在项目中,代码结构都不会如此简单,如果在开启子线程时,处处考虑 SynchronizationContext 实例线程传递的话,也比较麻烦。因此可以使用单例。
这里主要有两种方式实现:
- 普通单例缓存 SynchronizationContext 实例
- 继承自 SynchronizationContext 的单例实现
# 方式一
第一种比较好说,直接新建一个单例类,并将 SynchronizationContext 封装即可。
代码如下:
public class MySynchronizationContextHolder | |
{ | |
public static MySynchronizationContextHolder Instance { get; } = new MySynchronizationContextHolder(); | |
private SynchronizationContext _context = SynchronizationContext.Current; | |
private static int _mainThreadId = Thread.CurrentThread.ManagedThreadId; | |
/// <summary> | |
/// 子线程执行一个回调时调用 | |
/// </summary> | |
/// <param name="action"></param> | |
/// <param name="state"></param> | |
public void Post(Action action, object state) | |
{ | |
Post(new SendOrPostCallback((x) => action.Invoke()), state); | |
} | |
/// <summary> | |
/// 子线程执行一个回调时调用 | |
/// </summary> | |
/// <param name="callback"></param> | |
/// <param name="state"></param> | |
public void Post(SendOrPostCallback callback, object state) | |
{ | |
if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) | |
{ | |
callback.Invoke(state); | |
return; | |
} | |
_context.Post(callback, state); | |
} | |
} |
此种方式唯一需要注意的是:子线程调用实例之前,主线程必须先调用一次,确保单例初始化于主线程,否则初始化于子线程的单例会造成错误的结果。
(例如上述方式,若第一次由子线程初始化调用,会导致_context 实例为 Null,而且由于处于子线程甚至无法捕获错误。)
# 方式二
第二种方式,由于继承了 SynchronizationContext,就不能简单地通过调用 base.Post(callback,state);
这种方法来实现传递了。
因为那样的话,这个单例就会变成跟子线程调用一样的性质了:
直接调用的情况就如同子线程直接创建了一个新实例,然后调用造成的结果,跟子线程直接调用无所区别。
因此这里就需要额外做点工作,比如重写方法,缓存回调,然后在主线程 Update 中统一更新:
public class MySynchronizationContext : SynchronizationContext | |
{ | |
public static MySynchronizationContext Instance { get; } = new MySynchronizationContext(); | |
private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); | |
private Action _callback; | |
public void Update() | |
{ | |
if (_queue.Count == 0) return; | |
while (true) | |
{ | |
if (!_queue.TryDequeue(out _callback)) | |
return; | |
_callback.Invoke(); | |
} | |
} | |
/// <summary> | |
/// 子线程执行一个回调时调用 | |
/// </summary> | |
/// <param name="action"></param> | |
/// <param name="state"></param> | |
public void Post(Action action, object state) | |
{ | |
Post(new SendOrPostCallback((x) => action.Invoke()), state); | |
} | |
/// <summary> | |
/// 子线程执行一个回调时调用 | |
/// </summary> | |
/// <param name="callback"></param> | |
/// <param name="state"></param> | |
public override void Post(SendOrPostCallback callback, object state) | |
{ | |
_queue.Enqueue(() => callback.Invoke(state)); | |
} | |
} |
以上是两个可以将 SynchronizationContext 封装为单例的实现方式。
# 结束
个人感觉,这种一般在多线程使用中还是挺有用的 —— 比自己去循环查询结果来得方便。
此前个人就经常使用那种笨办法,比如制作 Unity 工具,在子线程回调中需要表现的时候:弹窗或者显示进度条,就是在使用回调修改值,然后在界面中再根据回调修改后的值判断是否进行显示,
中间转化及其麻烦,有时候甚至直接就因此放弃做什么表现了,直接一个子线程搞完了再统一给个回复。
后续就会考虑实际应用,或许修改下以前的代码了。
如 Github 上一些小工具,后续应该会考虑更新一波。