Permalink
Fetching contributors…
Cannot retrieve contributors at this time
384 lines (297 sloc) 17.4 KB

Cookbook for Visual Studio

Important for CPS extension authors and clients: please replace all references to ThreadHelper.JoinableTaskFactory with this.ThreadHandling.AsyncPump, where this.ThreadHandling is an [Import] IThreadHandling.

Initial setup

  • Add a reference to Microsoft.VisualStudio.Threading.

  • Add these using directives to your C# source file:

    using System.Threading.Tasks;
    using Microsoft.VisualStudio.Threading;
  • When using Microsoft.VisualStudio.Shell types in the same source file, import that namespace along with explicitly assigning Task to the .NET namespace so you can more conveniently use .NET Tasks:

    using Microsoft.VisualStudio.Shell;
    using Task = System.Threading.Tasks.Task;

Installing the analyzers

It is highly recommended that you install the Microsoft.VisualStudio.Threading.Analyzers NuGet package which adds analyzers to your project to help catch many common violations of the threading rules, helping you prevent your code from deadlocking.

How to switch to a background thread

await TaskScheduler.Default;

How to switch to the UI thread

In an async method

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

In a regular synchronous method

First, consider carefully whether you can make your method async first and then follow the above pattern. If you must remain synchronous, you can switch to the UI thread like this:

ThreadHelper.JoinableTaskFactory.Run(async delegate
{
    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
    // You're now on the UI thread.
});

How to switch to or use the UI thread with background priority

For those times when you need the UI thread but you don't want to introduce UI delays for the user, you can use the StartOnIdle extension method which will run your code on the UI thread when it is otherwise idle.

await ThreadHelper.JoinableTaskFactory.StartOnIdle(
    async delegate
    {
        for (int i = 0; i < 10; i++)
        {
            DoSomeWorkOn(i);

            // Ensure we frequently yield the UI thread in case user input is waiting.
            await Task.Yield();
        }
    });

Call async code from a synchronous method (and block on the result)

ThreadHelper.JoinableTaskFactory.Run(async delegate
{
    await SomeOperationAsync(...);
});

How to write a "fire and forget" method responsibly?

"Fire and forget" methods are async methods that are called without awaiting the Task that they (may) return.

Do not implement your fire and forget async method by returning async void. These methods will crash the process if they throw an unhandled exception.

There are two styles of "fire and forget":

void-returning fire and forget methods

These methods are designed to always be called in a fire and forget fashion, as the caller only gets a void return value and cannot await the async method even if it wanted to.

These methods should be named to indicate that they start an operation. For example, instead of calling the method Task DoSomethingAsync() you might call it void StartSomething().

Notwithstanding the void return type, you'll want to be async internally in order to actually do the work later instead of on your caller's callstack. But you also should be sure your async work finishes before your object claims to be disposed. You can accomplish both of these objectives using the JoinableTaskFactory.RunAsync method, and tacking on an extension method that captures failures and reports them for your analysis later. For the Visual Studio team's own internal use, the FileAndForget method can be tacked on at the end to send failure reports via VS telemetry fault events. You may want to handle your own exceptions within the async delegate as well.

void StartOperation()
{
  this.JoinableTaskFactory.RunAsync(async delegate
  {
    await Task.Yield(); // get off the caller's callstack.
    DoWork();
    this.DisposalToken.ThrowIfCancellationRequested();
    DoMoreWork();
  }).FileAndForget("vs/YOUR-FEATURE/YOUR-ACTION"); // Microsoft's own internal extension method
}

Where this.JoinableTaskFactory is defined by your AsyncPackage or, if you don't have one, by re-implementing this pattern in your own class:

class MyResponsibleType : IDisposable
{
  private readonly CancellationTokenSource disposeCancellationTokenSource = new CancellationTokenSource();

  internal MyResponsibleType()
  {
    this.JoinableTaskCollection = ThreadHelper.JoinableTaskContext.CreateCollection();
    this.JoinableTaskFactory = ThreadHelper.JoinableTaskContext.CreateFactory(this.JoinableTaskCollection);
  }

  JoinableTaskFactory JoinableTaskFactory { get; }
  JoinableTaskCollection JoinableTaskCollection { get; }

  /// <summary>
  /// Gets a <see cref="CancellationToken"/> that can be used to check if the package has been disposed.
  /// </summary>
  CancellationToken DisposalToken => this.disposeCancellationTokenSource.Token;

  public void Dispose()
  {
    this.Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool disposing)
  {
    if (disposing)
    {
      this.disposeCancellationTokenSource.Cancel();

      try
      {
        // Block Dispose until all async work has completed.
        ThreadHelper.JoinableTaskFactory.Run(this.JoinableTaskCollection.JoinTillEmptyAsync);
      }
      catch (OperationCanceledException)
      {
        // this exception is expected because we signaled the cancellation token
      }
      catch (AggregateException ex)
      {
        // ignore AggregateException containing only OperationCanceledException
        ex.Handle(inner => (inner is OperationCanceledException));
      }
    }
  }
}

Task-returning fire and forget methods

These are your typical async methods, but called without awaiting the resulting Task.

Not awaiting on the Task means that if that async method were to throw an exception (thus faulting the returned Task) that no one would notice. The error would go unreported and undetected -- until the side effects of the async method are discovered to be incomplete some other way. In a few cases this may be desirable. Since calling a Task-returning method without awaiting its result will often make C# emit a compiler warning, a void-returning Forget() extension method exists so you can tack it onto the end of an async method call to suppress the compiler warning, and make it clear to others reading the code that you are intentionally ignoring the result.

DoSomeWork();
DoSomeWorkAsync().Forget(); // no await here
DoABitMore();

Better than Forget() is to use FileAndForget(string) which will handle faulted Tasks by adding the error to the VS activity log and file a fault event for telemetry collection so the fault can be detected and fixed.

DoSomeWork();
DoSomeWorkAsync().FileAndForget("vs/YOUR-FEATURE/YOUR-ACTION"); // no await here
DoABitMore();

How can I await IVsTask?

Make sure you have the using directives defined at the top of this document. That adds a GetAwaiter extension method to IVsTask that makes it awaitable.

You can safely await an IVsTask in an ordinary C# async method or inside a JoinableTaskFactory.Run or RunAsync delegate.

How can I return IVsTask from a C# async method?

The preferred way is to use the JoinableTaskFactory.RunAsyncAsVsTask(Func<CancellationToken, Task>) extension method. This gives you a CancellationToken that is tied to IVsTask.Cancel().

IVsTask IVsYourNativeCompatibleInterface.DoAsync(string arg1)
{
    return ThreadHelper.JoinableTaskFactory.RunAsyncAsVsTask(
        VsTaskRunContext.UIThreadNormalPriority,
        cancellationToken => this.DoAsync(arg1, cancellationToken));
}

public async Task DoAsync(string arg1, CancellationToken cancellationToken)
{
    await Task.Yield();
}

In a pinch, where all you have is a JoinableTask, you can readily convert it to an IVsTask by calling the JoinableTask.AsVsTask() extension method as shown below. But this means the IVsTask.Cancel() method will not be able to communicate the cancellation request to the async delegate.

public IVsTask DoAsync()
{
    JoinableTask jt = ThreadHelper.JoinableTaskFactory.RunAsync(
        async delegate
        {
            await SomethingAsync();
            await SomethingElseAsync();
        });
    return jt.AsVsTask();
}

How can I block the UI thread for long or async work while displaying a (cancelable) wait dialog?

While it's generally preferable that you do work quickly or asynchronously so as to not block the user from interacting with the IDE, it is sometimes necessary and/or desirable to block user input. While doing so, it's good to provide feedback so the user knows the application hasn't hung. To display a dialog explaining what's going on while you're on and blocking the UI thread, and even give the user a chance to cancel the operation, you can call this extension method on JoinableTaskFactory:

ThreadHelper.JoinableTaskFactory.Run(
    "I'm churning on your data",
    async (progress, cancellationToken) =>
    {
        DoLongRunningWork();
        progress.Report(new ThreadedWaitDialogProgressData("Getting closer to being done.", isCancelable: true));
        await AndSomeAsyncWorkButBlockMainThreadAnyway(cancellationToken);
    });

If your operation cannot be canceled, just drop the CancellationToken parameter from your anonymous delegate and the dialog that is shown to the user will not include a Cancel button:

ThreadHelper.JoinableTaskFactory.Run(
    "I'm churning on your data",
    async (progress) =>
    {
        DoLongRunningWork();
        progress.Report(new ThreadedWaitDialogProgressData("Getting closer to being done."));
        await AndSomeAsyncWorkButBlockMainThreadAnyway();
    });

How can I initialize my VS package asynchronously?

As of Visual Studio 2015 (Dev14) you can define async VS packages, allowing binaries to be loaded on background threads and for your package to complete async work without blocking the UI thread as part of package load. Async Package 101

How do I make sure my async work has finished before my VS Package closes?

This is a very important topic because we know VS can crash on shutdown because async tasks or background threads still actively running after all VS packages have supposedly shutdown. The recommended pattern to solve this problem is to derive from AsyncPackage instead of Package. Then for any async work your package is responsible for starting but that isn't awaited on somewhere, you should start that work with your AsyncPackage's JoinableTaskFactory property, calling the RunAsync method. This ensures that on package close, your async work must complete before the AppDomain is shutdown. Your async work should generally also honor the AsyncPackage.DisposalToken and cancel itself right away when that token is signaled so that VS shutdown is not slowed down significantly by work that no longer matters.

How do I effectively verify that my code is fully free-threaded?

Code is free-threaded if it (and all code it invokes transitively) can complete on the caller's thread, no matter which thread that is. Code that is not free-threaded is said to be thread-affinitized, meaning some of its work may require execution on a particular thread.

It is not always necessary for code to be free-threaded. There are many cases where code must run on the main thread because it calls into other code that itself has thread-affinity or is not thread-safe. Important places to be free-threaded include:

  1. Code that runs in the MEF activation code paths (e.g. importing constructors, OnImportsSatisfied callbacks, and any code called therefrom).
  2. Code that may run on a background thread, especially when run within an async context.
  3. Static field initializers and static constructors.

In Visual Studio, thread-affinitized code typically requires the main thread (a.k.a. the UI thread). Code that has main thread affinity may appear to work when called from background threads for a few reasons, including:

  1. The code can sometimes run without requiring the main thread. For example, it only requires the main thread the first time it runs, and subsequent calls are effectively free-threaded.
  2. The code may switch itself to the main thread if its caller didn't call it on that thread. This approach only works if the main thread is available to service such a request to switch from a background thread.

The foregoing conditions can make it difficult to test whether your code is truly free-threaded. Knowing your code is free-threaded is important before executing it off the main thread, particularly when that code executes synchronously, since otherwise it can deadlock or contribute to threadpool starvation.

To test whether your code executes without any requirement on the main thread, you can run that code in VS within such a construct as this:

ThreadHelper.ThrowIfNotOnUIThread(); // this test only catches issues when it blocks the UI thread.
jtf.Run(async delegate
{
   using (jtf.Context.SuppressRelevance())
   {
      await Task.Run(delegate
      {
         var c = ActivateComponent();
         c.UseComponent();
      });
   }
});

This would effectively ensure that the main thread will not respond to any request from your test method, thus forcing a deadlock if one was lurking and could occur in otherwise non-deterministic conditions.

Try to execute such test code as early in the VS process as possible. This will help you catch any issues with code you may call that is thread affinitized the first time it is executed.

Note that being free-threaded is not the same thing as being thread-safe, which is an independent metric. Code can be free-threaded, thread-safe, both, or neither.

How do I effectively verify that my code is fully thread-safe?

Code is thread-safe if your code does not malfunction when called from more than one thread at a time.

It is not always necessary for code to be thread-safe. In fact many classes in the Base Class Library of .NET itself is not thread-safe. Writing thread-safe code involves mitigating data corruption, deadlocks, and higher level goals such as avoiding lock contention and threadpool starvation. Validating that code is thread-safe requires exhaustive reviews and testing, and thread-safety bugs can still slip through. Whether thread-safety should be a goal should typically be determined at the class level and clearly documented. Changing from thread-safe to non-thread-safe should be considered a breaking change. A class should be made thread-safe if being called from multiple threads at once is a supported scenario.

Techniques for writing thread-safe code include:

  1. Using synchronization primitives such as C# lock when accessing fields.
  2. Using immutable data structures and interlocked exchange methods while handling race conditions.

Techniques for verifying that code is thread-safe include both thorough code reviews and automated tests. Automated tests should execute your code concurrently. You may find you can shake out different bugs by testing with concurrency levels equal to Environment.ProcessorCount to maximize parallelism and throughput as well as a multiplier of that number (e.g. Environment.ProcessorCount * 3) so that hyper-switching of the CPU leads to different time slices and unique thread-safety bugs to be found. Such tests should verify that unexpected exceptions are not thrown, no hangs occur, and that the data at the conclusion of such concurrent execution is not corrupted.

Code can be made to run concurrently from a unit test using this technique:

int concurrencyLevel = Environment.ProcessorCount; // * 3
await Task.WhenAll(Enumerable.Range(1, concurrencyLevel).Select(i => Task.Run(delegate {
    CallYourCodeHere();
})));

If CallYourCodeHere() executes fast enough, it's possible that the above does not lead to actual concurrent execution since Task.Run does not guarantee that the delegate executes at a particular time. To increase the chances of concurrent execution, you can use the CountdownEvent class:

int concurrencyLevel = Environment.ProcessorCount; // * 3
var countdown = new CountdownEvent(concurrencyLevel);
await Task.WhenAll(Enumerable.Range(1, concurrencyLevel).Select(i => Task.Run(delegate {
    countdown.Signal();
    countdown.Wait();
    CallYourCodeHere();
})));

The above code will force allocation of a thread for each degree of concurrency you specify, and each thread will block until all threads are ready to go, at which point they will all unblock together (subject to kernel thread scheduling).

Note that being thread-safe is not the same thing as being free-threaded, which is an independent metric. Code can be free-threaded, thread-safe, both, or neither.