Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Allow VSGitExt to be constructed and subscribed to from background thread #1506

Closed
wants to merge 6 commits into from

Conversation

jcansdale
Copy link
Collaborator

@jcansdale jcansdale commented Feb 23, 2018

This PR allows the VSGitExt service to be constructed and subscribed to from background thread. This is necessary to allow the package from Show current PR on status bar #1396 to load on a background thread (package option AllowsBackgroundLoading = 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 the VSGitExt constructor, it was changed from RefreshActiveRepositories() to Task.Run(() => RefreshActiveRepositories).

The body of RefreshActiveRepositories can be changed to use sync, but this doesn't guarantee the order in which tasks start executing on the thread pool. For example:

        async Task Lock()
        {
            var refreshLock = new object();
            var writer = new StringWriter();
            var tasks = new List<Task>();

            for (int x = 0; x < 10; x++)
            {
                var y = x;
                tasks.Add(Task.Run(() =>
                {
                    lock (refreshLock)
                    {
                        writer.Write(y + ", ");
                    }
                }));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine(writer);
        }

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:

        async Task PreferFairness()
        {
            var refreshLock = new object();
            var writer = new StringWriter();
            var tasks = new List<Task>();

            for (int x = 0; x < 10; x++)
            {
                var y = x;
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    lock (refreshLock)
                    {
                        writer.Write(y + ", ");
                    }
                }, TaskCreationOptions.PreferFairness));
            }

            await Task.WhenAll(tasks);
            Console.WriteLine(writer);
        }

Which outputs: 1, 3, 4, 5, 6, 7, 8, 9, 2, 0,

An alternative approach is to use ContinueWith:

        async Task ContinueWith()
        {
            var writer = new StringWriter();
            var task = Task.CompletedTask;

            for (int x = 0; x < 10; x++)
            {
                var y = x;
                task = task.ContinueWith(_ => writer.Write(y + ", "), TaskScheduler.Default);
            }

            await task;
            Console.WriteLine(writer);
        }

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?

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.
@jcansdale jcansdale assigned shana and unassigned shana Feb 23, 2018
@jcansdale jcansdale requested a review from shana February 23, 2018 11:37
This will allow this service to be used by a package that is initializing on a background thread.
@jcansdale jcansdale changed the title Use Task.ContinueWith to guarantee ordering of RefreshActiveRepositories Allow VSGitExt to be constructed and subscribed to from background thread Feb 26, 2018

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.
Copy link
Contributor

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 ;)

Copy link
Collaborator Author

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())
Copy link
Contributor

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?

Copy link
Collaborator Author

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);
Copy link
Contributor

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?

Copy link
Collaborator Author

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());
Copy link
Contributor

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?

Copy link
Collaborator Author

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>();
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

@jcansdale
Copy link
Collaborator Author

This PR has been merged into and extended by #1510. I'll close this one and continue development there.

@jcansdale jcansdale closed this Feb 28, 2018
@jcansdale jcansdale deleted the fixes/1493-using-ContinueWith branch March 23, 2018 12:17
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants