BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News .NET 6: Threading Improvements

.NET 6: Threading Improvements

This item in japanese

Bookmarks

While numerous libraries exist to abstract away the complexities of asynchronous and concurrent programming, developers still need to drop down to lower thread-handling logic from time to time. Continuing our API changes for .NET 6 series, we look at some new tricks for multi-threading.

Async Parallel.ForEach

When the Parallel class was created, C# didn’t have language support for asynchronous programming. While there was the IAsyncResult pattern from .NET 1.1, it wasn’t widely used and the vast majority of code was designed to be executed synchronously.

This has become a problem as the focus has shifted to asynchronous code using async/await. Currently there is no built-in support to start a Parallel.ForEach operation and asynchronously wait for the result. GSPP writes,

I am very active on Stack Overflow and I see people needing this all the time. People then use very bad workarounds such as starting all items in parallel at the same time, then WhenAll them. So they start 10000 HTTP calls and wonder why it performs so poorly. Or, they execute items in batches. This can be much slower because as the batch completes item by item the effective DOP [degrees of parallelism] decreases. Or, they write very awkward looping code with collections of tasks and weird waiting schemes.

To address this concern, a set of Parallel.ForEachAsync functions were created. Each take either an IEnumerable or IAsyncEnumerable. Parallel options and a cancellation token may also be provided.

public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)

Parallel.ForEachAsync is one of the rare cases where ValueTask is being used instead of Task. The basic idea here is since values are going to be processed in a tight loop, the extra overhead of creating a full Task object isn’t justified. Stephen Toub writes,

I think the funcs should return ValueTask rather than Task. The primary concern with ValueTask is that it'll be consumed incorrectly, but here the consumer is the method we're implementing, and we'll just make sure to do it right ;) And returning ValueTask is more accomodating: Task can be converted to a ValueTask very cheaply, but it requires an allocation to convert a ValueTask (if it's not already wrapping a Task) into a Task.

While overall this feature was well received, one point of contention was whether or not developers should be required to provide a degree of parallelism value. This roughly equates to the number of threads that will be assigned to the parallel operation.

It was decided since most developers would not know the ideal degree of parallelism for their workload, expecting them to provide one would be counter-productive. The default selected is Environment.ProcessorCount.

Thread.ManagedThreadId Deprecated

The property Environment.CurrentManagedThreadId was introduced in .NET 4.5 to be a more efficient alternative to Thread.ManagedThreadId property. However, this was never communicated in the documentation and developers continue to use Thread.ManagedThreadId.

In order to guide developers towards the better option, a code analysis warning has been added for Thread.ManagedThreadId.

While this effectively means Thread.ManagedThreadId is deprecated, it is not being marked as obsolete. Developers may continue to use it in the foreseeable future even though Environment.CurrentManagedThreadId is now preferred.

Thread.UnsafeStart

The new function for starting threads is called “unsafe” because it does not capture the execution context. David Fowler explains,

We added UnsafeStart in this PR #46181 because we needed to lazily create thread pool threads and the timer thread on the default execution context. UnsafeStart avoids capturing the current execution context and restoring it when the thread runs. There are other places where we create threads that could use similar logic

This function is already seeing use in numerous places including,

  • FileSystemWatcher for OS X
  • SocketAsyncEngine for Unix
  • CounterGroup in the Tracing APIs
  • ThreadPoolTaskScheduler when the task is marked as LongRunning

When running in the browser, UnsafeStart will throw a PlatformNotSupportedException.

Periodic Timer

The PeriodicTimer class was originally called AsyncTimer because it is designed to be used in an asynchronous context. As you can see in the example below, an await must be used between each tick of the timer.

var second = TimeSpan.FromSeconds(1);
using var timer = new AsyncTimer(second);
while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine($"Tick {DateTime.Now}")
}

Fowler explains the design benefits of the PeriodicTimer,

This API only makes sense for timers that fire repeatedly, timers that fire once could be Task based (we already have Task.Delay for this).

The timer will be paused while user code is executing and will resume the next period once it ends.

The timer can be stopped using a CancellationToken provided to stop the enumeration.

The execution context isn't captured.

The timer can also be stopped by calling Stop or Dispose, even while a WaitForNextTickAsync call is currently being executed.

There are already five other timers in .NET, but none of them have this particular set of features. As part of the documentation, a new guide for choosing which timer to use is being planned.

Trivia: The PeriodicTimer class will be the first timer to share a namespace with another timer. Previously each timer was placed in a separate namespace:

  • System.Timers.Timer
  • System.Threading.Timer
  • System.Windows.Forms.Timer
  • System.Web.UI.Timer
  • System.Windows.Threading.DispatcherTimer

For our previous reports in the series, see the links below:

Rate this Article

Adoption
Style

BT