Skip to content
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

fixing up some threading cases #2419

Merged

Conversation

jmarolf
Copy link
Contributor

@jmarolf jmarolf commented Jun 10, 2017

_tasksService.LoadedProject() assumes that all logic run inside the using is correct and never causes a deadlock. _tasksService.LoadedProjectAsync explicitly uses JTF to ensure no deadlocks occur.

GetService blocks the calling thread where as GetServiceAsync will try several techniques to acquire the service in a non-blocking way before falling back to a blocking approach.

AddSubscriptionsAsync was locking but then calling asynchronus methods and not waiting for them to complete. This could have led to races or interactions with the project after it had closed.

@davkean to double check my understanding of this threading code

Ask mode template forthcoming

@jmarolf
Copy link
Contributor Author

jmarolf commented Jun 10, 2017

@dotnet/project-system for review

@@ -78,7 +78,7 @@ internal abstract class CrossTargetSubscriptionHostBase : OnceInitializedOnceDis

protected async Task AddInitialSubscriptionsAsync()
{
using (_tasksService.LoadedProject())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So previously the object returned by LoadedProject would have been disposed at the end of the block. Do we need to do something similar to the object returned by LoadedProjectAsync?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No _tasksService.LoadedProject() blocked until it got a lock, releasing the lock when it was disposed. _tasksService.LoadedProjectAsync asynchronously waits for a lock, executes the lambda, and then released the lock.


In reply to: 121492671 [](ancestors = 121492671)

@@ -249,7 +249,7 @@ await subscriber.Value.InitializeSubscriberAsync(this, _activeConfiguredProjectS

await _commonServices.ThreadingService.SwitchToUIThread();

using (_tasksService.LoadedProject())
await _tasksService.LoadedProjectAsync(() =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigureAwait(true) for this await?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadedProjectAsync returns a JoinableTask which always resumes onto the same thread as its caller.


In reply to: 121514747 [](ancestors = 121514747)

@davkean
Copy link
Member

davkean commented Jun 13, 2017

Can we split this into 3 separate commits?

@jmarolf jmarolf force-pushed the async-useage/use-async-apis-when-possible branch from e2bee26 to dc4e581 Compare June 13, 2017 17:45
@jmarolf
Copy link
Contributor Author

jmarolf commented Jun 13, 2017

@davkean split this into separate commits.

@@ -262,10 +262,12 @@ await subscriber.Value.InitializeSubscriberAsync(this, _activeConfiguredProjectS

foreach (var subscriber in Subscribers)
{
subscriber.Value.AddSubscriptionsAsync(newProjectContext);
Shell.ThreadHelper.JoinableTaskFactory.Run(()=> subscriber.Value.AddSubscriptionsAsync(newProjectContext));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its unclear to me what the correct behavior is here. This section of code is in a lock yet previously we would call subscriber.Value.AddSubscriptionsAsync(newProjectContext) and not observe the returned task or wait for it to complete. That seems to imply that we don't care about the lock, since we previously exited the lock statement without any guarantee that the async tasks had finished. Here I am making the assumption that this was an error and we only want to exit the lock once all subscriptions have been added.

@@ -262,10 +262,12 @@ await subscriber.Value.InitializeSubscriberAsync(this, _activeConfiguredProjectS

foreach (var subscriber in Subscribers)
{
subscriber.Value.AddSubscriptionsAsync(newProjectContext);
Shell.ThreadHelper.JoinableTaskFactory.Run(() => subscriber.Value.AddSubscriptionsAsync(newProjectContext));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What problem are we solving here? You're adding a synchronous blocking call in a threadpool thread, which is the cause of all these issues here: #2297.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we're not observing it - why isn't this just an await?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you cannot await inside a lock in C# (for obvious reasons).

Its unclear to me what the correct thing to do here is. Previously, we were in a lock but we did not wait for async operations to complete before leaving the lock.

either:

  1. This code does not need to be in the lock, in which case we should move it out of the lock
  2. This code does need to be in the lock, in which case we need to actually wait for the async operations to complete or we risk bugs from threading behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is why we're using semaphores elsewhere. Is this actually fixing a known race? if not, I'd leave it until I rewrite this logic of coordination between configured projects. CPS want us to approach it a different way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually fixing a known race

I am not aware of one,. Sounds like this is 2 and we should not exit the lock until this subscription is finished.

Which is why we're using semaphores elsewhere

Can you show me where else in the code we do that?

I'd leave it until I rewrite this logic of coordination between configured projects

Fair enough, do we have an issue # for how CPS wants us to do this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See _gate in LanguageServiceHost.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks a lot better, any objections to just doing that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have objectiins to doing in 15.3 unless we're solving an actual existing bug. This becomes easy to introduce other issues and other bugs such as deadlocks on shutdown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ie We've regressed so many times around this code - i'd prefer not to change it unless it's fixing a bug that we're running into right now.

@@ -266,14 +265,16 @@ private JoinableTask ExecuteWithinLockAsync(Func<Task> task)

_projectConfigurationsWithSubscriptions.Add(configuredProject.ProjectConfiguration);
}
}

return Task.CompletedTask;
Copy link
Member

@davkean davkean Jun 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the differ confused me - I see it's for func, not the parent method

Copy link
Member

@davkean davkean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to remove the JoinableTaskFactory.Run.

@srivatsn
Copy link
Contributor

If this is not addressing any known perf regressions then let's retarget this for 15.6

@srivatsn srivatsn changed the base branch from master to dev15.6 June 17, 2017 00:35
@jmarolf jmarolf force-pushed the async-useage/use-async-apis-when-possible branch from b6e81da to a004d10 Compare June 19, 2017 22:31
@jmarolf
Copy link
Contributor Author

jmarolf commented Jun 20, 2017

@davkean can you comment on whether you want the remaining changes or not?

@davkean
Copy link
Member

davkean commented Jun 20, 2017

You pulled the semaphore? I'm okay with the other changes.

@jmarolf
Copy link
Contributor Author

jmarolf commented Jun 20, 2017

@davkean yeah, the semaphore code definitely has problems but lets tackle that in a different PR.

@jmarolf jmarolf merged commit 6b982a4 into dotnet:dev15.6 Jun 20, 2017
@jmarolf jmarolf deleted the async-useage/use-async-apis-when-possible branch October 1, 2018 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants