阅读本篇文章前需要C#多线程基础、Async函数基础、SynchronizationContext基础
以下是测试代码:
static void Main(string[] args)
{
Test0();
Console.Read();
}
static void Test0()
{
AsyncMethod();
Console.WriteLine("after calling AsyncMethod : " + Thread.CurrentThread.ManagedThreadId);
}
static async Task AsyncMethod()
{
Console.WriteLine("before await : " + Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
// 一个耗时的计算
ConsumeTime();
Console.WriteLine("Task : " + Thread.CurrentThread.ManagedThreadId);
});
Console.WriteLine("after await : " + Thread.CurrentThread.ManagedThreadId);
}
执行结果:
可以看出,await后面的代码段是由执行Task的线程来执行的。
但资料上的理论知识还表示,由于在Task执行完之前,Thread1会挂起状态机返回执行Console.WriteLine("after calling AsyncMethod : " + Thread.CurrentThread.ManagedThreadId);,等到Task结束后,由Thread3恢复状态机,执行await后的代码,这种挂起操作是有消耗的。如果Task很快就执行完了,在Thread1返回之前就已经执行好了,这种情况下,如果还将状态机挂起,就白白损耗了性能。它很聪明,Thread1检测到自己返回前Task已经执行完毕,就不会将状态机挂起,并且由自己直接完成await后的代码,而不是交给Thread3来执行。这种情况我没有写代码检测。
在默认情况下,await后面的代码段是由执行Task的线程来执行的,但当await一个TaskCompletionSource的Task时,由于这个Task并不是真的需要去执行,所以也就没有所谓的执行Task的线程,那么这种情况下谁来执行await后的代码段呢?
以下是测试代码:
static void Main(string[] args)
{
Test1();
Console.Read();
}
static void Test1()
{
AsyncTest();
Task.Run(() =>
{
SetResult();
});
}
static async Task AsyncTest()
{
Console.WriteLine("before await : " + Thread.CurrentThread.ManagedThreadId);
m_tcs = new TaskCompletionSource();
await m_tcs.Task;
Console.WriteLine("after await : " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
}
static void SetResult()
{
Console.WriteLine("before SetResult : " + Thread.CurrentThread.ManagedThreadId);
m_tcs.SetResult(10);
Console.WriteLine("after SetResult : " + Thread.CurrentThread.ManagedThreadId);
}
private static TaskCompletionSource m_tcs;
执行结果如下:
由此可以看出,使用TaskCompletionSource时,await后的代码段由调用SetResult方法的线程执行。需要注意的是,调用SetResult方法的线程会先转去执行await后的代码,然后再接着执行SetResult后的代码。
我写了一个SynchronizationContext来让TaskScheduler按照我定的规则来调度Task,当一个线程在await我的SynchronizationContext负责调度的Task时,按理说,我的SynchronizationContext是保证这个Task被调度的一个机制,那么await后的代码段该由谁来执行呢?
下面是我的SynchronizationContext代码,它将需要执行的Task先放到自己的队列里,然后在Tick方法中再去执行这些Task需要执行的操作。
///
/// 在Tick中调度Task的SynchronizationContext
///
public class TickBaseSynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
m_taskQueue.Enqueue(new KeyValuePair(d, state));
}
public override void Send(SendOrPostCallback d, object state)
{
m_taskQueue.Enqueue(new KeyValuePair(d, state));
}
///
/// 驱动task调度
///
///
public bool Tick()
{
KeyValuePair item;
if (m_taskQueue.TryDequeue(out item))
{
try
{
item.Key(item.Value);
}
catch (Exception e)
{
EventOnException(e);
}
return true;
}
return false;
}
///
/// 通知使用者调度的task抛出异常
///
public event Action EventOnException;
///
/// 调度队列
///
private ConcurrentQueue> m_taskQueue = new ConcurrentQueue>();
}
下面是测试代码:
static void Main(string[] args)
{
Test3();
Console.Read();
}
static void Test3()
{
Task.Run(() =>
{
ExecuteTasks();
});
SyncCtxAsyncMethod();
Console.Read();
}
///
/// 调度SynchronizationContext的Task
///
static void ExecuteTasks()
{
while (true)
{
if (m_syncCtx != null && !m_syncCtx.Tick())
{
Thread.Sleep(1000);
}
}
}
///
/// 让SynchronizationContext来调度一个task并await它
///
static async Task SyncCtxAsyncMethod()
{
// 首先设置SynchronizationContext和TaskScheduler
m_syncCtx = new TickBaseSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(m_syncCtx);
m_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Console.WriteLine("before await : " + Thread.CurrentThread.ManagedThreadId);
// 这个Task是会被SynchronizationContext调度的
await Task.Factory.StartNew(() =>
{
Console.WriteLine("Task : " + Thread.CurrentThread.ManagedThreadId);
}, CancellationToken.None, TaskCreationOptions.LongRunning, m_taskScheduler);
Console.WriteLine("after await : " + Thread.CurrentThread.ManagedThreadId);
}
private static TickBaseSynchronizationContext m_syncCtx;
private static TaskScheduler m_taskScheduler;
结果如下:
结果显示,执行Task和执行await后的代码的线程是同一个线程,这其实是一个错误的结论,在这个例子中,SynchronizationContext采用的规则是将所有队列里的Task都使用同一个线程串行调度,这个例子太片面了,可以修改SynchronizationContext的调度规则,比如修改为将队列里的Task都用线程池的线程调度,多写几个不同规则的SynchronizationContext,你会得出正确的结论:await后的代码会封装成一个新的Task由SynchronizationContext按照自己的规则调度执行。