Skip to content

Commit 2b40a61

Browse files
authored
Merge pull request #599 from AArnott/addNonConcurrentSyncContext
Add NonConcurrentSynchronizationContext
2 parents cbf19ee + 1446742 commit 2b40a61

File tree

5 files changed

+468
-0
lines changed

5 files changed

+468
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.VisualStudio.Threading;
8+
using Microsoft.VisualStudio.Threading.Tests;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
public class NonConcurrentSynchronizationContextTests : TestBase
13+
{
14+
private readonly NonConcurrentSynchronizationContext nonSticky = new NonConcurrentSynchronizationContext(sticky: false);
15+
16+
public NonConcurrentSynchronizationContextTests(ITestOutputHelper logger)
17+
: base(logger)
18+
{
19+
}
20+
21+
[Fact]
22+
public void CreateCopy()
23+
{
24+
var copy = this.nonSticky.CreateCopy();
25+
Assert.NotSame(this.nonSticky, copy);
26+
ConfirmNonConcurrentPost(this.nonSticky, copy);
27+
}
28+
29+
[Fact]
30+
public void Post_NonConcurrentExecution()
31+
{
32+
ConfirmNonConcurrentPost(this.nonSticky, this.nonSticky);
33+
}
34+
35+
[Fact]
36+
public void Send_NonConcurrentExecution()
37+
{
38+
ConfirmNonConcurrentSend(this.nonSticky);
39+
}
40+
41+
[Fact]
42+
public void UnhandledException_WithNoHandler()
43+
{
44+
// Verifies that no crash occurs when an exception is thrown without an event handler attached.
45+
this.nonSticky.Post(s => throw new InvalidOperationException(), null);
46+
}
47+
48+
[Fact]
49+
public async Task UnhandledException_WithHandler()
50+
{
51+
var eventArgs = new TaskCompletionSource<(object?, Exception)>();
52+
this.nonSticky.UnhandledException += (s, e) => eventArgs.SetResult((s, e));
53+
this.nonSticky.Post(s => throw new InvalidOperationException(), null);
54+
var (sender, ex) = await eventArgs.Task.WithCancellation(this.TimeoutToken);
55+
Assert.Same(this.nonSticky, sender);
56+
Assert.IsType<InvalidOperationException>(ex);
57+
}
58+
59+
[Fact]
60+
public async Task NonSticky_HasNoCurrentSyncContext()
61+
{
62+
Assert.Null(await GetCurrentSyncContextDuringPost(this.nonSticky));
63+
}
64+
65+
[Fact]
66+
public async Task Sticky_SetsCurrentSyncContext()
67+
{
68+
var sticky = new NonConcurrentSynchronizationContext(sticky: true);
69+
Assert.Same(sticky, await GetCurrentSyncContextDuringPost(sticky));
70+
}
71+
72+
[Fact]
73+
public async Task InlinedSendReappliesSyncContextWhenSticky()
74+
{
75+
var sticky = new NonConcurrentSynchronizationContext(sticky: true);
76+
var tcs = new TaskCompletionSource<object?>();
77+
sticky.Post(_ =>
78+
{
79+
try
80+
{
81+
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
82+
sticky.Send(_ =>
83+
{
84+
Assert.Same(sticky, SynchronizationContext.Current);
85+
}, null);
86+
Assert.IsType<SynchronizationContext>(SynchronizationContext.Current);
87+
tcs.TrySetResult(null);
88+
}
89+
catch (Exception ex)
90+
{
91+
tcs.TrySetException(ex);
92+
}
93+
}, null);
94+
await tcs.Task.WithCancellation(this.TimeoutToken);
95+
}
96+
97+
[Fact]
98+
public async Task InlinedSendIgnoresSyncContextWhenNotSticky()
99+
{
100+
var tcs = new TaskCompletionSource<object?>();
101+
this.nonSticky.Post(_ =>
102+
{
103+
try
104+
{
105+
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
106+
this.nonSticky.Send(_ =>
107+
{
108+
Assert.IsType<SynchronizationContext>(SynchronizationContext.Current);
109+
}, null);
110+
Assert.IsType<SynchronizationContext>(SynchronizationContext.Current);
111+
tcs.TrySetResult(null);
112+
}
113+
catch (Exception ex)
114+
{
115+
tcs.TrySetException(ex);
116+
}
117+
}, null);
118+
await tcs.Task.WithCancellation(this.TimeoutToken);
119+
}
120+
121+
[Fact]
122+
public async Task SyncContextIsNotInherited()
123+
{
124+
// Set a SyncContext when creating the context, and ensure the current context is never used.
125+
SynchronizationContext.SetSynchronizationContext(new ThrowingSyncContext());
126+
var nonConcurrentContext = new NonConcurrentSynchronizationContext(sticky: false);
127+
128+
// Also confirm the current context doesn't impact the Post method.
129+
Assert.Null(await GetCurrentSyncContextDuringPost(nonConcurrentContext).ConfigureAwait(false));
130+
}
131+
132+
[Fact]
133+
public void Send_RethrowsExceptionFromDelegate()
134+
{
135+
bool handlerInvoked = false;
136+
this.nonSticky.UnhandledException += (s, e) => handlerInvoked = true;
137+
138+
Assert.Throws<InvalidOperationException>(() => this.nonSticky.Send(_ => throw new InvalidOperationException(), null));
139+
Assert.False(handlerInvoked);
140+
}
141+
142+
[Fact]
143+
public void Send_RethrowsExceptionFromDelegate_WhenInlined()
144+
{
145+
bool handlerInvoked = false;
146+
this.nonSticky.UnhandledException += (s, e) => handlerInvoked = true;
147+
148+
this.nonSticky.Send(_ => Assert.Throws<InvalidOperationException>(() => this.nonSticky.Send(_ => throw new InvalidOperationException(), null)), null);
149+
Assert.False(handlerInvoked);
150+
}
151+
152+
[Fact]
153+
public void SendWithinSendDoesNotDeadlock()
154+
{
155+
Task.Run(delegate
156+
{
157+
bool reachedInnerDelegate = false;
158+
this.nonSticky.Send(_ =>
159+
{
160+
this.nonSticky.Send(_ =>
161+
{
162+
reachedInnerDelegate = true;
163+
}, null);
164+
}, null);
165+
166+
Assert.True(reachedInnerDelegate);
167+
}).WithCancellation(this.TimeoutToken).GetAwaiter().GetResult();
168+
}
169+
170+
[Fact]
171+
public async Task SendWithinPostDoesNotDeadlock()
172+
{
173+
var reachedInnerDelegate = new TaskCompletionSource<bool>();
174+
this.nonSticky.Post(_ =>
175+
{
176+
this.nonSticky.Send(_ =>
177+
{
178+
reachedInnerDelegate.SetResult(true);
179+
}, null);
180+
}, null);
181+
182+
Assert.True(await reachedInnerDelegate.Task.WithCancellation(this.TimeoutToken));
183+
}
184+
185+
[Fact]
186+
public void CannotFoolSendBySettingSyncContext()
187+
{
188+
using var releaseFirst = new ManualResetEventSlim();
189+
this.nonSticky.Post(s =>
190+
{
191+
releaseFirst.Wait(UnexpectedTimeout);
192+
}, null);
193+
using var secondEntered = new ManualResetEventSlim();
194+
Task sendTask = Task.Run(delegate
195+
{
196+
SynchronizationContext.SetSynchronizationContext(this.nonSticky);
197+
this.nonSticky.Send(s =>
198+
{
199+
secondEntered.Set();
200+
}, null);
201+
});
202+
Assert.False(secondEntered.Wait(ExpectedTimeout));
203+
204+
// Now that we've proven the second one hasn't started, allow the first to finish and confirm the second one could then execute.
205+
releaseFirst.Set();
206+
Assert.True(secondEntered.Wait(UnexpectedTimeout));
207+
sendTask.Wait(UnexpectedTimeout);
208+
}
209+
210+
private static Task<SynchronizationContext?> GetCurrentSyncContextDuringPost(SynchronizationContext synchronizationContext)
211+
{
212+
var observed = new TaskCompletionSource<SynchronizationContext?>();
213+
synchronizationContext.Post(
214+
s =>
215+
{
216+
observed.SetResult(SynchronizationContext.Current);
217+
},
218+
null);
219+
return observed.Task;
220+
}
221+
222+
private static void ConfirmNonConcurrentSend(SynchronizationContext ctxt)
223+
{
224+
using var releaseFirst = new ManualResetEventSlim();
225+
ctxt.Post(s =>
226+
{
227+
releaseFirst.Wait(UnexpectedTimeout);
228+
}, null);
229+
using var secondEntered = new ManualResetEventSlim();
230+
Task sendTask = Task.Run(delegate
231+
{
232+
ctxt.Send(s =>
233+
{
234+
secondEntered.Set();
235+
}, null);
236+
});
237+
Assert.False(secondEntered.Wait(ExpectedTimeout));
238+
239+
// Now that we've proven the second one hasn't started, allow the first to finish and confirm the second one could then execute.
240+
releaseFirst.Set();
241+
Assert.True(secondEntered.Wait(UnexpectedTimeout));
242+
sendTask.Wait(UnexpectedTimeout);
243+
}
244+
245+
private static void ConfirmNonConcurrentPost(SynchronizationContext a, SynchronizationContext b)
246+
{
247+
// Verify that the two instances share a common non-concurrent queue
248+
// by scheduling something on each one, and confirming that the second can't start
249+
// before the second one completes.
250+
using var releaseFirst = new ManualResetEventSlim();
251+
a.Post(s =>
252+
{
253+
releaseFirst.Wait(UnexpectedTimeout);
254+
}, null);
255+
using var secondEntered = new ManualResetEventSlim();
256+
b.Post(s =>
257+
{
258+
secondEntered.Set();
259+
}, null);
260+
Assert.False(secondEntered.Wait(ExpectedTimeout));
261+
262+
// Now that we've proven the second one hasn't started, allow the first to finish and confirm the second one could then execute.
263+
releaseFirst.Set();
264+
Assert.True(secondEntered.Wait(UnexpectedTimeout));
265+
}
266+
267+
private class ThrowingSyncContext : SynchronizationContext
268+
{
269+
public override void Post(SendOrPostCallback d, object? state) => throw new NotSupportedException();
270+
271+
public override void Send(SendOrPostCallback d, object? state) => throw new NotSupportedException();
272+
}
273+
}

0 commit comments

Comments
 (0)