Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
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. |
grokys
Feb 26, 2018
Contributor
"This means the service to be constructed and subscribed to" isn't very grammarful English ;)
"This means the service to be constructed and subscribed to" isn't very grammarful English ;)
jcansdale
Feb 26, 2018
Author
Collaborator
Ooops. I wish VS had an English grammar checker!
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()) |
grokys
Feb 26, 2018
Contributor
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?
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?
jcansdale
Feb 26, 2018
Author
Collaborator
This can be on a background or Main thread. We explicitly change to a background or Main thread when required.
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); |
grokys
Feb 26, 2018
Contributor
I think there could still be a (very small chance of a) race here if PropertyChanged is called concurrently by 2 threads, I think?
I think there could still be a (very small chance of a) race here if PropertyChanged is called concurrently by 2 threads, I think?
jcansdale
Feb 26, 2018
Author
Collaborator
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.
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()); |
grokys
Feb 26, 2018
Contributor
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?
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?
jcansdale
Feb 26, 2018
Author
Collaborator
This line should be part of the initial PendingTasks (see PendingTasks = InitializeAsync() above). When PropertyChange events arrive, they should be executed after it.
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>(); |
shana
Feb 27, 2018
Collaborator
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.
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.
jcansdale
Feb 27, 2018
Author
Collaborator
I'll do something similar in this PR. I can avoid switching threads by using IAsyncServiceProvider to retrieve IGitExt.
I'll do something similar in this PR. I can avoid switching threads by using IAsyncServiceProvider to retrieve IGitExt.
|
This PR has been merged into and extended by #1510. I'll close this one and continue development there. |
This PR allows the
VSGitExtservice 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
RefreshActiveRepositoriesconsistent when I moved it off the main thread to the thread pool. In theVSGitExtconstructor, it was changed fromRefreshActiveRepositories()toTask.Run(() => RefreshActiveRepositories).The body of
RefreshActiveRepositoriescan 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.PreferFairnessflag 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
ContinueWithmethod is used.What do you think?