-
Notifications
You must be signed in to change notification settings - Fork 0
Concurrency issue when there is no lock #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Changes from all commits
e0861f5
77bae41
b7664a9
b16c7bd
e976125
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ | |
| using Samples.Mvc5.EfModelFirst; | ||
| using Samples.Mvc5.EFCodeFirst; | ||
| using Samples.Mvc5.Helpers; | ||
| using System.Threading.Tasks; | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Samples.Mvc5.Controllers | ||
| { | ||
|
|
@@ -131,6 +133,35 @@ public ActionResult About() | |
| /// <returns>The <see cref="ActionResult"/>.</returns> | ||
| public ActionResult ResultsAuthorization() => View(); | ||
|
|
||
| public async Task<ActionResult> CallAsync() | ||
| { | ||
| var profiler = MiniProfiler.Current; | ||
| var service = new FooService(); | ||
| using (profiler.Step("action")) | ||
| { | ||
| var tasks = new List<Task>(); | ||
| for (int i = 0; i < 10; i++) | ||
| { | ||
| tasks.Add(service.FooAsync()); | ||
| } | ||
| await Task.WhenAll(tasks); | ||
| return Content("All good"); | ||
| } | ||
| } | ||
|
|
||
| class FooService | ||
| { | ||
| public async Task FooAsync() | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the method simulates a typical service method. Following typical pattern the service implementation is using ConfigureAwait(false). |
||
| { | ||
| var profiler = MiniProfiler.Current; | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MVC sample is using WebRequestProfilerProvider which relies on HttpContext to carry the miniprofiler instance. Since this is a continuation after ConfigureAwait(false) the synchronization context together with the HttpContext will be removed here. So if I don't use an explicit reference to profiler here the step would not be recorded at all. DefaultProfilerProvider (and its possible future merge with WebRequestProfilerProvider) would fix this issue by using AsyncLocal to carry the mini profiler instance. However the execution of the continuation is not synchronized with continuations from other 9 calls to FooAsync that are made from the same CallAsync call (and they all share the same instance of MiniProfiler). As a result Timing.AddChild might be invoked concurrently. This is the exact problem that i'm trying to prevent. |
||
| await Task.Delay(10).ConfigureAwait(false); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continuations of this await will execute in parallel since await is using ConfigureAwait(false) and the calling method is using Task.WhenAll instead of awaiting individual executions. |
||
| using (profiler.Step("foo async")) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MVC sample is using WebRequestProfilerProvider which relies on HttpContext to carry the miniprofiler instance. Since this is a continuation after ConfigureAwait(false) the synchronization context together with the HttpContext will be removed here. So if I don't use an explicit reference to profiler here the step would not be recorded at all. DefaultProfilerProvider (and its possible future merge with WebRequestProfilerProvider) would fix this issue by using AsyncLocal to carry the mini profiler instance. However the execution of the continuation is not synchronized with continuations from other 9 calls to FooAsync that are made from the same CallAsync call (and they all share the same instance of MiniProfiler). As a result Timing.AddChild might be invoked concurrently. This is the exact problem that i'm trying to prevent. |
||
| { | ||
| await Task.Delay(10).ConfigureAwait(false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// fetch the route hits. | ||
| /// </summary> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| using System.Linq; | ||
| using System.Runtime.Serialization; | ||
| using StackExchange.Profiling.Helpers; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| #if NET45 | ||
| using System.Web.Script.Serialization; | ||
| #endif | ||
|
|
@@ -22,6 +24,9 @@ public class Timing : IDisposable | |
| private readonly decimal? _minSaveMs; | ||
| private readonly bool _includeChildrenWithMinSave; | ||
|
|
||
| // used for concurrent access detection | ||
| private int _threadsAccessing = 0; | ||
|
|
||
| /// <summary> | ||
| /// Initialises a new instance of the <see cref="Timing"/> class. | ||
| /// Obsolete - used for serialization. | ||
|
|
@@ -253,19 +258,44 @@ public void Stop() | |
| /// </remarks> | ||
| public void AddChild(Timing timing) | ||
| { | ||
| if (Children == null) | ||
| if (Interlocked.Increment(ref _threadsAccessing) > 1) | ||
| throw new Exception("Concurrent access"); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this throws with almost every request to CallAsync |
||
|
|
||
| Task.Delay(10).Wait(); | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an artificial delay added to increase the chance of concurrency
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thread.Sleep I guess is not available in .netcore (or perhaps i need to install a separate package). |
||
|
|
||
| try | ||
| { | ||
| if (Children == null) | ||
| Children = new List<Timing>(); | ||
|
|
||
| Children.Add(timing); | ||
| if(timing.Profiler == null) | ||
| timing.Profiler = Profiler; | ||
| timing.ParentTiming = this; | ||
| timing.ParentTimingId = Id; | ||
| if (Profiler != null) | ||
| timing.MiniProfilerId = Profiler.Id; | ||
| Children.Add(timing); | ||
| if(timing.Profiler == null) | ||
| timing.Profiler = Profiler; | ||
| timing.ParentTiming = this; | ||
| timing.ParentTimingId = Id; | ||
| if (Profiler != null) | ||
| timing.MiniProfilerId = Profiler.Id; | ||
| } | ||
| finally | ||
| { | ||
| Interlocked.Decrement(ref _threadsAccessing); | ||
| } | ||
| } | ||
|
|
||
| internal void RemoveChild(Timing timing) => Children?.Remove(timing); | ||
| internal void RemoveChild(Timing timing) | ||
| { | ||
| if (Interlocked.Increment(ref _threadsAccessing) > 1) | ||
| throw new Exception("Concurrent access"); | ||
|
|
||
| try | ||
| { | ||
| Children?.Remove(timing); | ||
| } | ||
| finally | ||
| { | ||
| Interlocked.Decrement(ref _threadsAccessing); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds <paramref name="customTiming"/> to this <see cref="Timing"/> step's dictionary of | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this action simulates a typical async action that invokes an async service method (FooAsync). The service method might access some data sources (like redis, database etc) and measure the performance of individual calls.
The action also invokes multiple service calls and waits for completion all of them. This is also quite legitimate pattern, which parallelizes execution of the service methods to increase the action's response time