2.1 什么是协成
(黑色字体为熊猫,猫大原话。红色自己理解)
说到协成,我们先了解什么是异步,异步简单来说就是,我要发起一个调用,但是这个被调用方(可能是其他线程,也可能是IO)出结果需要一段时间,我不想这个调用阻塞竹调用方的整个线程,因此传给被调用方一个回调函数,被调用方运行完成后回调这个回调函数就能通知调用方继续往下执行。
static void Main(string[] args)
{
int loopCount = 0;
while (true)
{
Thread.Sleep(1);
++loopCount;
if (loopCount % 10000==0)
{
Console.WriteLine($"loop count:{loopCount}");
}
}
}
这时我需要加个功能,在程序一开始,我希望在5秒钟之后打印出loopCount的值。看到5秒后我们可以想到Sleep方法,它会阻塞线程一定时间然后继续执行。我们显然不能在主线程中Sleep,因为会破坏掉每10000次计数打印一次的逻辑。
private static int loopCount = 0;
static void Main(string[] args)
{
OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
WaitTimeAsync(5000,WaitTimeFinishCallback);
while (true)
{
OneThreadSynchronizationContext.Instance.Update();
Thread.Sleep(1);
++loopCount;
if (loopCount % 10000==0)
{
Console.WriteLine($"loop count:{loopCount}");
}
}
}
/// <summary>
/// 回调函数
/// </summary>
private static void WaitTimeFinishCallback()
{
Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");
}
private static void WaitTimeAsync(int waitTime,Action action)
{
Thread thread = new Thread(()=> WaitTime(waitTime,action));
thread.Start();
}
/// <summary>
/// 在另外的线程等待
/// </summary>
private static void WaitTime(int waitTime,Action action)
{
Thread.Sleep(waitTime);
// 将action扔回主线程
OneThreadSynchronizationContext.Instance.Post(o=>action(),null);
}
我们这里设计了一个WaitTimeAsync方法,WaitTimeAsync其实就是一个典型的异步方法,它从竹线程发起调用,传入了一个WaitTimeFinishCallback回调方法做参数,开启了一个线程,线程Sleep一定时间后,将传过来的回调扔回到竹线程程序执行。OneThreadSynchronizationContext是一个跨线程队列,任何线程可以往里面扔委托。OneThreadSynchronizationContext的Update方法在主线程中调用,会将这些委托取出来放在主线程执行,为什么回调方法需要扔回到主线程执行呢?隐晦回调方法中读取了loopCount,loopCount在主线程中也有读写,所以要么加锁,要么永远保证只在主线程中读取。加锁是个不好的做法,代码中到处都是锁会导致阅读跟维护困难,很容易产生多线程bug。这中将逻辑打包成委托然后扔回另外一个线程多线程开发中常用的技巧。
我们可能又需要改动需求,WaitTimeFinishCallback执行完成之后,再想等3秒,再打印一下loopCount.
/// <summary>
/// 回调函数
/// </summary>
private static void WaitTimeFinishCallback()
{
Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");
WaitTimeAsync(3000, WaitTimeFinishCallback2);
}
private static void WaitTimeFinishCallback2()
{
Console.WriteLine($"WaitTimeAsync finish loopCount的值是:{loopCount}");
}
private static void WaitTimeAsync(int waitTime,Action action)
{
Thread thread = new Thread(()=> WaitTime(waitTime,action));
thread.Start();
}
我们这是可能仍然需要改需求,三秒后继续,接下来四秒继续打印。这样的话如同上面我们还需要写个回调函数,在三秒结束后调用。这样插入代码,显得非常繁琐。这里可以回答什么是协成,。
:
OneThreadSynchronizationContext类,他继承了SynchronizationContext类,而SyncehronizationContext提供在各种同步模型中传播同步上下文的基础公共。
。
public class OneThreadSynchronizationContext:SynchronizationContext
{
/// <summary>
/// 单列
/// </summary>
public static OneThreadSynchronizationContext Instance { get; } = new OneThreadSynchronizationContext();
/// <summary>
/// 线程Id
/// </summary>
private readonly int mainThreadid = Thread.CurrentThread.ManagedThreadId;
/// <summary>
/// 线程同步队列 (回调的方法)
/// </summary>
private readonly ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();
/// <summary>
/// 委托
/// </summary>
private Action a;
public void Update()
{
while (true)
{
// 出队成功,执行委托。否则结束这次判断
if (!this.queue.TryDequeue(out a))
{
return;
}
a();
}
}
/// <summary>
/// 回调方法如队
/// </summary>
/// <param name="callback"></param>
/// <param name="state"></param>
public override void Post(SendOrPostCallback callback, object state)
{
// 当前线程是主线程这直接执行回调函数,否则加入回调队列
if (Thread.CurrentThread.ManagedThreadId==this.mainThreadid)
{
callback(state);
return;
}
this.queue.Enqueue(() => { callback(state); });
}
}
更好的协成:异步
上文讲了一串回调就是协成,显然这样写代码,增加逻辑,插入逻辑非常容易出错。我们需要利用异步语法把这个异步回调的形式改成同步的像是,幸好C#已经帮我们设计好了。
private static int loopCount = 0;
static void Main(string[] args)
{
OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
//WaitTimeAsync(5000,WaitTimeFinishCallback);
Console.WriteLine($"主线程:{Thread.CurrentThread.ManagedThreadId}");
Crontine();
while (true)
{
OneThreadSynchronizationContext.Instance.Update();
Thread.Sleep(1);
++loopCount;
if (loopCount % 10000==0)
{
Console.WriteLine($"loop count:{loopCount}");
}
}
}
private static async void Crontine()
{
await WatiTimeAsync(5000);
Console.WriteLine($"1当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
await WatiTimeAsync(4000);
Console.WriteLine($"2当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
await WatiTimeAsync(3000);
Console.WriteLine($"3当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
}
private static Task WatiTimeAsync(int waitTime)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
Thread thread = new Thread(()=> WaitTime(waitTime,tcs)); // 开启一个线程
thread.Start();
return tcs.Task;
}
/// <summary>
/// 另外一个线程操作
/// </summary>
/// <param name="waitTime"></param>
/// <param name="tcs"></param>
private static void WaitTime(int waitTime,TaskCompletionSource<bool> tcs)
{
Thread.Sleep(waitTime);
// 将tcs扔回主线程
OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true),null);
}
在这段代码里,WaitTimeAsync方法中,我们利用了TaskCompletionSource类替代了之前传入的Action参数,WaitTimeAsync方法反悔了一个Task类型的结果。WaitTime中我们把action()替换成了tcs.SetResult(true),WaitTimeAsync方法钱使用await关键字,这样可以将一连串的回调改成同步形式()。这样一来代码显得十分简洁,开发起来也方便多了。
这里还有个技巧,我们发现WaitTime中需要将tcs.SetResult扔回到主线程执行,微软给我们提供了一种简单的方法,在主线程设置好同步上下文
SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);
在WaitTime中直接调用tcs.SetResult(true)就行了,回调会自动扔到同步上下文中,而同步上下文我们可以在主线程中取出回调执行,这样自动能够完成回到主线程的操作。
private static int loopCount = 0;
static void Main(string[] args)
{
//OneThreadSynchronizationContext _ = OneThreadSynchronizationContext.Instance;
// 微软提供同步上下文
SynchronizationContext.SetSynchronizationContext(OneThreadSynchronizationContext.Instance);
//WaitTimeAsync(5000,WaitTimeFinishCallback);
Console.WriteLine($"主线程:{Thread.CurrentThread.ManagedThreadId}");
Crontine();
while (true)
{
OneThreadSynchronizationContext.Instance.Update();
Thread.Sleep(1);
++loopCount;
if (loopCount % 10000==0)
{
Console.WriteLine($"loop count:{loopCount}");
}
}
}
private static async void Crontine()
{
await WatiTimeAsync(5000);
Console.WriteLine($"1当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
await WatiTimeAsync(4000);
Console.WriteLine($"2当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
await WatiTimeAsync(3000);
Console.WriteLine($"3当前线程:{Thread.CurrentThread.ManagedThreadId},WaitTimeAsync finsih loopCount的值是:{loopCount}");
}
private static Task WatiTimeAsync(int waitTime)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
Thread thread = new Thread(()=> WaitTime(waitTime,tcs)); // 开启一个线程
thread.Start();
return tcs.Task;
}
/// <summary>
/// 另外一个线程操作
/// </summary>
/// <param name="waitTime"></param>
/// <param name="tcs"></param>
private static void WaitTime(int waitTime,TaskCompletionSource<bool> tcs)
{
Thread.Sleep(waitTime);
// 已经设置同步上下文
tcs.SetResult(true);
// 将tcs扔回主线程
//OneThreadSynchronizationContext.Instance.Post(o=>tcs.SetResult(true),null);
}
如果不设置同步上下文,你会发现打印出来当前线程就不是主线程,这也是很多第三方库跟.net core内置库的用法,默认不回调到主线程,所以我们使用的时候需要设置下同步上下文。其实这个设计本人觉的没有必要,交由库的开发者去实现更好,尤其是在游戏开发中,逻辑全部是单线程的,回调每次都走一遍同步上下文显得多余了,所以ET框架提供了不使用同步上下文的实现ETTask,diam更显的简洁高效。
注:
async:。
await:
task:
TaskCompletionSource<TResult>: