Allow VSGitExt to be constructed and subscribed to from background thread #1506
Conversation
Lock prevent tasks being executed at the same time but doesn't stop them being started out of order.
PendingTasks is used to execute tasks in order on the thread pool.
This will allow this service to be used by a package that is initializing on a background thread.
|
||
namespace GitHub.VisualStudio.Base | ||
{ | ||
/// <summary> | ||
/// This service acts as an always available version of <see cref="IGitExt"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// Initialization for this service will be done asynchronously and the <see cref="IGitExt" /> service will be | ||
/// retrieved on the Main thread. This means the service to be constructed and subscribed to on a background thread. |
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 means the service to be constructed and subscribed to" isn't very grammarful English ;)
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.
Ooops. I wish VS had an English grammar checker!
{ | ||
// Refresh ActiveRepositories on background thread so we don't delay startup. | ||
InitializeTask = Task.Run(() => RefreshActiveRepositories()); | ||
if (!context.IsActive || !await TryInitialize()) |
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.
So should we be on a b/g thread for this? Or are we now saying that it can be run on the UI thread too?
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 can be on a background or Main thread. We explicitly change to a background or Main thread when required.
if (gitService != null) | ||
{ | ||
gitService.PropertyChanged += (s, e) => | ||
{ | ||
if (e.PropertyName == nameof(gitService.ActiveRepositories)) | ||
{ | ||
RefreshActiveRepositories(); | ||
// Execute tasks in sequence using thread pool (TaskScheduler.Default). | ||
PendingTasks = PendingTasks.ContinueWith(_ => RefreshActiveRepositories(), TaskScheduler.Default); |
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.
I think there could still be a (very small chance of a) race here if PropertyChanged
is called concurrently by 2 threads, I think?
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.
I'm pretty sure the issue before was with RefreshActiveRepositories
being called from the constructor and the PropertyChanged
event at the same time.
It was called with Task.Run
from the constructor here:
https://github.com/github/VisualStudio/blob/master/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs#L53
...and the PropertyChanged
event here:
https://github.com/github/VisualStudio/blob/master/src/GitHub.TeamFoundation.14/Services/VSGitExt.cs#L86
I don't think we need to worry about the PropertyChanged
event being called concurrently by 2 threads.
} | ||
}; | ||
|
||
// Do this after we start listening so we don't miss an event. | ||
await Task.Run(() => RefreshActiveRepositories()); |
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.
Given that PropertyChanged
can be called on any thread, isn't there still a chance of a race here when team explorer is loaded because this isn't using PendingTasks
?
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 line should be part of the initial PendingTasks
(see PendingTasks = InitializeAsync()
above). When PropertyChange
events arrive, they should be executed after it.
{ | ||
// GetService must be called from the Main thread. | ||
await ThreadingHelper.SwitchToMainThreadAsync(); | ||
return serviceProvider.GetService<T>(); |
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.
The way we're exposing MEF implementations as services is by having a MEF shim that redirects to the real instance instead, like we do for GitHubServiceProvider, UsageTracker (https://github.com/github/VisualStudio/blob/docs/clarify-tokens/src/GitHub.VisualStudio/Services/UsageTrackerDispatcher.cs and https://github.com/github/VisualStudio/blob/docs/clarify-tokens/src/GitHub.VisualStudio/Services/UsageTracker.cs) and registering the service in the ServiceProviderPackage
. This is how this one should also probably be registered so we don't have to keep switching threads to get at it.
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.
I'll do something similar in this PR. I can avoid switching threads by using IAsyncServiceProvider
to retrieve IGitExt
.
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.
I've done as you've suggested in #1510. This PR is now obsolete.
This PR has been merged into and extended by #1510. I'll close this one and continue development there. |
This PR allows the
VSGitExt
service to be constructed and subscribed to from background thread. This is necessary to allow the package fromShow current PR on status bar
#1396 to load on a background thread (package optionAllowsBackgroundLoading = true
).Notes on using Task.ContinueWith to guarantee ordering of RefreshActiveRepositories
I wanted to understand what I could have done to keep the ordering of
RefreshActiveRepositories
consistent when I moved it off the main thread to the thread pool. In theVSGitExt
constructor, it was changed fromRefreshActiveRepositories()
toTask.Run(() => RefreshActiveRepositories)
.The body of
RefreshActiveRepositories
can be changed to usesync
, but this doesn't guarantee the order in which tasks start executing on the thread pool. For example:Might output:
0, 3, 4, 5, 6, 7, 8, 9, 1, 2,
Interestingly the
TaskCreationOptions.PreferFairness
flag doesn't seem to make any difference when starting tasks:Which outputs:
1, 3, 4, 5, 6, 7, 8, 9, 2, 0,
An alternative approach is to use
ContinueWith
:Which consistently outputs:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
I prefer this approach because the intended ordering is explicit when the
ContinueWith
method is used.What do you think?