nos*_*tio 20 .net c# multithreading task-parallel-library async-await
这会有点长,所以请耐心等待.
我在想默认任务scheduler(ThreadPoolTaskScheduler
)的行为与默认的" ThreadPool
" SynchronizationContext
(后者可以通过await
或显式地通过隐式引用)非常相似TaskScheduler.FromCurrentSynchronizationContext()
.它们都安排在随机ThreadPool
线程上执行的任务.实际上,SynchronizationContext.Post
只是打电话ThreadPool.QueueUserWorkItem
.
但是,TaskCompletionSource.SetResult
当从默认排队的任务中使用时,工作方式有一个微妙但重要的区别SynchronizationContext
.这是一个简单的控制台应用程序说明它:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleTcs
{
class Program
{
static async Task TcsTest(TaskScheduler taskScheduler)
{
var tcs = new TaskCompletionSource<bool>();
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
tcs.SetResult(true);
Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
},
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler);
Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await tcs.Task.ConfigureAwait(true);
Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await task.ConfigureAwait(true);
Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId);
}
// Main
static void Main(string[] args)
{
// SynchronizationContext.Current is null
// install default SynchronizationContext on the thread
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
// use TaskScheduler.Default for Task.Factory.StartNew
Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.Default).Wait();
// use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew
Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait();
Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}
}
Run Code Online (Sandbox Code Playgroud)
输出:
Test #1, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after await tcs.Task, thread: 10 after tcs.SetResult, thread: 10 after await task, thread: 10 Test #2, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after tcs.SetResult, thread: 10 after await tcs.Task, thread: 11 after await task, thread: 11 Press enter to exit, thread: 9
这是一个控制台应用程序,Main
默认情况下它的线程没有任何同步上下文,所以我在运行测试之前显式安装了默认的一个:SynchronizationContext.SetSynchronizationContext(new SynchronizationContext())
.
最初,我认为我在测试#1(安排任务的地方)期间完全理解了执行工作流程TaskScheduler.Default
.有tcs.SetResult
同步调用第一延续部分(await tcs.Task
),则执行点返回到tcs.SetResult
并且永远继续同步,包括第二await task
.这对我来说很有意义,直到我意识到以下情况.由于我们现在已经在线程上安装了默认同步上下文await tcs.Task
,因此应该捕获它并且延迟应该异步发生(即,在排队的不同池线程上SynchronizationContext.Post
).依此类推,如果我从一个WinForms应用程序内运行测试#1,它会被异步后继续await tcs.Task
,对WinFormsSynchronizationContext
在消息循环的未来迭代.
但这不是测试#1中发生的事情.出于好奇,我改变ConfigureAwait(true)
了ConfigureAwait(false)
,这对输出没有任何影响.我正在寻找对此的解释.
现在,在测试#2(任务被安排TaskScheduler.FromCurrentSynchronizationContext()
)期间,与#1相比,确实还有一个线程切换.从输出中可以看出,await tcs.Task
触发的延续tcs.SetResult
确实是在另一个池线程上异步发生的.我也试过ConfigureAwait(false)
,也没有改变任何东西.我也尝试SynchronizationContext
在开始测试#2之前立即安装,而不是在开始时安装.这导致了完全相同的输出.
我实际上更喜欢测试#2的行为,因为它为副作用(可能是死锁)留下了更少的间隙,这可能是由同步延续触发引起的tcs.SetResult
,即使它是以额外的线程切换为代价的.但是,我不完全理解为什么这样的线程切换发生在哪里ConfigureAwait(false)
.
我熟悉以下关于这个主题的优秀资源,但我仍然在寻找对测试#1和#2中所见行为的一个很好的解释.有人可以详细说明吗?
TaskCompletionSource
并行编程的本质:任务调度程序和同步上下文
并行编程:TaskScheduler.FromCurrentSynchronizationContext
它是关于SynchronizationContext的全部内容
[更新]我的观点是,在线程命中await tcs.Task
测试#1中的第一个之前,默认同步上下文对象已显式安装在主线程上.IMO,它不是一个GUI同步上下文的事实并不意味着它不应该被捕获继续之后await
.这就是为什么我希望继续tcs.SetResult
发生在不同的线程上ThreadPool
(在那里排队SynchronizationContext.Post
),而主线程可能仍然被阻止TcsTest(...).Wait()
.这与此处描述的场景非常相似.
所以,我继续实施哑同步上下文类 TestSyncContext
,这是只是一个包装周围SynchronizationContext
.它现在安装而不是SynchronizationContext
自己:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleTcs
{
public class TestSyncContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine("TestSyncContext.Post, thread: " + Thread.CurrentThread.ManagedThreadId);
base.Post(d, state);
}
public override void Send(SendOrPostCallback d, object state)
{
Console.WriteLine("TestSyncContext.Send, thread: " + Thread.CurrentThread.ManagedThreadId);
base.Send(d, state);
}
};
class Program
{
static async Task TcsTest(TaskScheduler taskScheduler)
{
var tcs = new TaskCompletionSource<bool>();
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
tcs.SetResult(true);
Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
},
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler);
Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await tcs.Task.ConfigureAwait(true);
Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await task.ConfigureAwait(true);
Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId);
}
// Main
static void Main(string[] args)
{
// SynchronizationContext.Current is null
// install default SynchronizationContext on the thread
SynchronizationContext.SetSynchronizationContext(new TestSyncContext());
// use TaskScheduler.Default for Task.Factory.StartNew
Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.Default).Wait();
// use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew
Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait();
Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}
}
Run Code Online (Sandbox Code Playgroud)
神奇的是,事情发生了变化!这是新的输出:
Test #1, thread: 10 before await tcs.Task, thread: 10 before tcs.SetResult, thread: 6 TestSyncContext.Post, thread: 6 after tcs.SetResult, thread: 6 after await tcs.Task, thread: 11 after await task, thread: 6 Test #2, thread: 10 TestSyncContext.Post, thread: 10 before await tcs.Task, thread: 10 before tcs.SetResult, thread: 11 TestSyncContext.Post, thread: 11 after tcs.SetResult, thread: 11 after await tcs.Task, thread: 12 after await task, thread: 12 Press enter to exit, thread: 10
现在,测试#1现在按预期运行(await tcs.Task
异步排队到池线程).#2似乎也没问题.让我们ConfigureAwait(true)
改为ConfigureAwait(false)
:
Test #1, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 10 after await tcs.Task, thread: 10 after tcs.SetResult, thread: 10 after await task, thread: 10 Test #2, thread: 9 TestSyncContext.Post, thread: 9 before await tcs.Task, thread: 9 before tcs.SetResult, thread: 11 after tcs.SetResult, thread: 11 after await tcs.Task, thread: 10 after await task, thread: 10 Press enter to exit, thread: 9
测试#1仍然能够正常运作预期:ConfigureAwait(false)
使await tcs.Task
忽略同步上下文(TestSyncContext.Post
调用了),所以现在同步之后继续tcs.SetResult
.
为什么这与SynchronizationContext
使用默认值的情况不同?我仍然很想知道.也许,默认任务调度程序(负责await
连续)检查线程同步上下文的运行时类型信息,并给出一些特殊处理SynchronizationContext
?
现在,我还是无法解释测试#2的行为ConfigureAwait(false)
.这是一个较少的TestSyncContext.Post
电话,这是理解.但是,await tcs.Task
仍然会在不同的线程上继续tcs.SetResult
(与#1不同),这不是我所期望的.我仍然在寻找这个原因.
Ste*_*ary 19
当您开始深入了解实现细节时,区分记录/可靠行为和未记录的行为非常重要.而且,SynchronizationContext.Current
设置为真的并不合适new SynchronizationContext()
; 某些类型的.NET治疗null
作为默认的调度,以及其他类型的治疗null
或 new SynchronizationContext()
作为默认的调度.
当你await
不完整时Task
,TaskAwaiter
默认情况下捕获当前SynchronizationContext
- 除非是null
(或它的GetType
返回typeof(SynchronizationContext)
),在这种情况下TaskAwaiter
捕获当前TaskScheduler
.此行为主要记录在案(该GetType
条款不是AFAIK).但请注意,这描述了行为TaskAwaiter
,而不是TaskScheduler.Default
或TaskFactory.StartNew
.
捕获上下文(如果有)后,await
计划继续.如我的博客所述(此行为未记录),此计划继续使用ExecuteSynchronously
.但是,请注意ExecuteSynchronously
并不总是同步执行 ; 特别是,如果一个continuation有一个任务调度程序,它只会请求在当前线程上同步执行,并且任务调度程序可以选择拒绝同步执行它(也是未记录的).
最后,请注意,TaskScheduler
可以请求同步执行任务,但SynchronizationContext
不能.因此,如果await
捕获自定义SynchronizationContext
,则它必须始终异步执行延续.
所以,在你原来的测试#1中:
StartNew
使用默认任务调度程序(在线程10上)启动新任务.SetResult
同步执行由await tcs.Task
.设置的连续.StartNew
任务结束时,它同步执行设置的延续await task
.在您原来的测试#2中:
StartNew
使用任务调度程序包装器为默认构造的同步上下文(在线程10上)启动新任务.请注意,线程10上的任务已TaskScheduler.Current
设置为a,SynchronizationContextTaskScheduler
其m_synchronizationContext
实例是由new SynchronizationContext()
; 创建的; 但是,那个线程SynchronizationContext.Current
是null
.SetResult
尝试await tcs.Task
在当前任务调度程序上同步执行延续; 但是,它可以因为无法SynchronizationContextTaskScheduler
看到该线程10拥有SynchronizationContext.Current
的null
是需要一段时间new SynchronizationContext()
.因此,它异步地调度连续(在线程11上).StartNew
任务结束时; 在这种情况下,我认为await task
继续在同一个线程上是巧合.总之,我必须强调,取决于无证实施细节并不明智.如果你想让你的async
方法继续在一个线程池线程,然后将其包装在一个Task.Run
.这将使您的代码的意图更加清晰,并使您的代码对未来的框架更新更具弹性.此外,不要设置SynchronizationContext.Current
到new SynchronizationContext()
,因为那种情况下的处理是不一致的.
归档时间: |
|
查看次数: |
9105 次 |
最近记录: |