Skip to content

Commit 6d16fb4

Browse files
authored
Add NoMessagePumpSyncContext..ctor(SynchronizationContext) for Post/Send behaviors (#1578)
Previously, this class would always schedule work to the threadpool. This doesn't match requirements where the non-pumping code expects to schedule work on the main thread.
2 parents b02c569 + bb3388f commit 6d16fb4

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,33 @@ public class NoMessagePumpSyncContext : SynchronizationContext
2020
/// </summary>
2121
private static readonly SynchronizationContext DefaultInstance = new NoMessagePumpSyncContext();
2222

23+
private readonly SynchronizationContext? underlyingSyncContext;
24+
2325
/// <summary>
2426
/// Initializes a new instance of the <see cref="NoMessagePumpSyncContext"/> class.
2527
/// </summary>
28+
/// <remarks>
29+
/// When using this constructor, <see cref="Post"/> uses the default <see cref="SynchronizationContext"/>
30+
/// behavior and schedules work on the thread pool, while <see cref="Send"/> uses the default
31+
/// <see cref="SynchronizationContext"/> behavior and invokes the callback synchronously on the calling thread.
32+
/// </remarks>
2633
public NoMessagePumpSyncContext()
2734
{
2835
// This is required so that our override of Wait is invoked.
2936
this.SetWaitNotificationRequired();
3037
}
3138

39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="NoMessagePumpSyncContext"/> class.
41+
/// </summary>
42+
/// <param name="underlyingSyncContext">The <see cref="SynchronizationContext"/> that should handle calls to <see cref="Post"/> and <see cref="Send"/>.</param>
43+
public NoMessagePumpSyncContext(SynchronizationContext underlyingSyncContext)
44+
: this()
45+
{
46+
Requires.NotNull(underlyingSyncContext, nameof(underlyingSyncContext));
47+
this.underlyingSyncContext = underlyingSyncContext;
48+
}
49+
3250
/// <summary>
3351
/// Gets a shared instance of this class.
3452
/// </summary>
@@ -37,6 +55,36 @@ public static SynchronizationContext Default
3755
get { return DefaultInstance; }
3856
}
3957

58+
/// <inheritdoc/>
59+
public override void Send(SendOrPostCallback d, object? state)
60+
{
61+
Requires.NotNull(d, nameof(d));
62+
63+
if (this.underlyingSyncContext is { } underlying)
64+
{
65+
underlying.Send(d, state);
66+
}
67+
else
68+
{
69+
base.Send(d, state);
70+
}
71+
}
72+
73+
/// <inheritdoc/>
74+
public override void Post(SendOrPostCallback d, object? state)
75+
{
76+
Requires.NotNull(d, nameof(d));
77+
78+
if (this.underlyingSyncContext is { } underlying)
79+
{
80+
underlying.Post(d, state);
81+
}
82+
else
83+
{
84+
base.Post(d, state);
85+
}
86+
}
87+
4088
/// <summary>
4189
/// Synchronously blocks without a message pump.
4290
/// </summary>

test/Microsoft.VisualStudio.Threading.Tests/NoMessagePumpSyncContextTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Threading;
6+
using System.Threading.Tasks;
67

78
/// <summary>
89
/// Tests for <see cref="NoMessagePumpSyncContext"/>.
@@ -36,6 +37,109 @@ public void Default_IsNoMessagePumpSyncContext()
3637
Assert.IsType<NoMessagePumpSyncContext>(NoMessagePumpSyncContext.Default);
3738
}
3839

40+
/// <summary>
41+
/// Verifies that <see cref="NoMessagePumpSyncContext.Post"/> schedules work on the thread pool
42+
/// when no underlying sync context is provided.
43+
/// </summary>
44+
[Fact]
45+
public async Task Post_DefaultConstructor_ExecutesOnThreadPool()
46+
{
47+
NoMessagePumpSyncContext sc = new();
48+
TaskCompletionSource<bool> tcs = new();
49+
sc.Post(_ => tcs.SetResult(Thread.CurrentThread.IsThreadPoolThread), null);
50+
Assert.True(await tcs.Task.WithCancellation(this.TimeoutToken));
51+
}
52+
53+
/// <summary>
54+
/// Verifies that <see cref="NoMessagePumpSyncContext.Send"/> executes work synchronously
55+
/// on the calling thread when no underlying sync context is provided.
56+
/// </summary>
57+
[Fact]
58+
public void Send_DefaultConstructor_ExecutesInlineOnCallingThread()
59+
{
60+
NoMessagePumpSyncContext sc = new();
61+
int callingThreadId = Thread.CurrentThread.ManagedThreadId;
62+
int? callbackThreadId = null;
63+
bool callbackInvoked = false;
64+
65+
sc.Send(
66+
_ =>
67+
{
68+
callbackInvoked = true;
69+
callbackThreadId = Thread.CurrentThread.ManagedThreadId;
70+
},
71+
null);
72+
73+
Assert.True(callbackInvoked);
74+
Assert.Equal(callingThreadId, callbackThreadId);
75+
}
76+
77+
/// <summary>
78+
/// Verifies that <see cref="NoMessagePumpSyncContext(SynchronizationContext)"/> throws
79+
/// <see cref="ArgumentNullException"/> when a null underlying context is passed.
80+
/// </summary>
81+
[Fact]
82+
public void Constructor_WithNullUnderlyingContext_Throws()
83+
{
84+
Assert.Throws<ArgumentNullException>(() => new NoMessagePumpSyncContext(null!));
85+
}
86+
87+
/// <summary>
88+
/// Verifies that <see cref="NoMessagePumpSyncContext.Post"/> rejects a null callback before
89+
/// delegating to the underlying sync context.
90+
/// </summary>
91+
[Fact]
92+
public void Post_WithNullCallback_Throws()
93+
{
94+
ThrowingSyncContext underlying = new();
95+
NoMessagePumpSyncContext sc = new(underlying);
96+
97+
Assert.Throws<ArgumentNullException>(() => sc.Post(null!, null));
98+
Assert.False(underlying.PostInvoked);
99+
}
100+
101+
/// <summary>
102+
/// Verifies that <see cref="NoMessagePumpSyncContext.Send"/> rejects a null callback before
103+
/// delegating to the underlying sync context.
104+
/// </summary>
105+
[Fact]
106+
public void Send_WithNullCallback_Throws()
107+
{
108+
ThrowingSyncContext underlying = new();
109+
NoMessagePumpSyncContext sc = new(underlying);
110+
111+
Assert.Throws<ArgumentNullException>(() => sc.Send(null!, null));
112+
Assert.False(underlying.SendInvoked);
113+
}
114+
115+
/// <summary>
116+
/// Verifies that <see cref="NoMessagePumpSyncContext.Post"/> delegates to the underlying
117+
/// sync context when one is provided.
118+
/// </summary>
119+
[Fact]
120+
public async Task Post_WithUnderlyingContext_DelegatesToUnderlying()
121+
{
122+
TaskCompletionSource<bool> tcs = new();
123+
RecordingPostSyncContext underlying = new(posted: _ => tcs.SetResult(true));
124+
NoMessagePumpSyncContext sc = new(underlying);
125+
sc.Post(_ => { }, null);
126+
Assert.True(await tcs.Task.WithCancellation(this.TimeoutToken));
127+
}
128+
129+
/// <summary>
130+
/// Verifies that <see cref="NoMessagePumpSyncContext.Send"/> delegates to the underlying
131+
/// sync context when one is provided.
132+
/// </summary>
133+
[Fact]
134+
public void Send_WithUnderlyingContext_DelegatesToUnderlying()
135+
{
136+
bool sendInvoked = false;
137+
RecordingSendSyncContext underlying = new(sent: _ => sendInvoked = true);
138+
NoMessagePumpSyncContext sc = new(underlying);
139+
sc.Send(_ => { }, null);
140+
Assert.True(sendInvoked);
141+
}
142+
39143
#if NETFRAMEWORK
40144
/// <summary>
41145
/// Establishes the baseline: on a plain STA thread without a special synchronization context,
@@ -77,4 +181,50 @@ public void Wait_BlocksComRpcCalls()
77181
}
78182
}
79183
#endif
184+
185+
/// <summary>
186+
/// A <see cref="SynchronizationContext"/> that invokes a callback when <see cref="Post"/> is called.
187+
/// </summary>
188+
private class RecordingPostSyncContext(Action<SendOrPostCallback> posted) : SynchronizationContext
189+
{
190+
public override void Post(SendOrPostCallback d, object? state)
191+
{
192+
posted(d);
193+
base.Post(d, state);
194+
}
195+
}
196+
197+
/// <summary>
198+
/// A <see cref="SynchronizationContext"/> that invokes a callback when <see cref="Send"/> is called.
199+
/// </summary>
200+
private class RecordingSendSyncContext(Action<SendOrPostCallback> sent) : SynchronizationContext
201+
{
202+
public override void Send(SendOrPostCallback d, object? state)
203+
{
204+
sent(d);
205+
base.Send(d, state);
206+
}
207+
}
208+
209+
/// <summary>
210+
/// A <see cref="SynchronizationContext"/> that records whether <see cref="Post"/> or <see cref="Send"/> were invoked.
211+
/// </summary>
212+
private class ThrowingSyncContext : SynchronizationContext
213+
{
214+
public bool PostInvoked { get; private set; }
215+
216+
public bool SendInvoked { get; private set; }
217+
218+
public override void Post(SendOrPostCallback d, object? state)
219+
{
220+
this.PostInvoked = true;
221+
throw new InvalidOperationException();
222+
}
223+
224+
public override void Send(SendOrPostCallback d, object? state)
225+
{
226+
this.SendInvoked = true;
227+
throw new InvalidOperationException();
228+
}
229+
}
80230
}

0 commit comments

Comments
 (0)