© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
N. VermeirIntroducing .NET 6https://doi.org/10.1007/978-1-4842-7319-7_11

11. Advanced .NET 6

Nico Vermeir1  
(1)
Merchtem, Belgium
 

A lot of the things in .NET that we use on a daily bases are often taken for granted. We reserve memory to store data, and we just assume that that memory gets released at some point. We ask the framework for a thread and we get one, but where does that thread come from? And how does async work again? Let’s go into some more detail and explore how these concepts actually work.

Garbage Collector

One of the greater advantages of writing managed code is the access to a garbage collector, or GC. A garbage collector manages memory usage for you; it allocates memory when requested and releases memory automatically when no longer in use. This helps greatly in preventing out of memory issues; it does not eliminate the risk completely; we as developers need to be smart about memory allocation as well, but it is a great help.

Before we dive into the garbage collector, let’s refresh our memory about memory. Memory consists of two pieces, a stack and a heap. A misconception that has been going around is that one is for value types and the other is for reference types; that is not entirely correct. Reference types do always go on the heap, but value types go where they are declared. Let me clarify.

Every time we call a method, a frame is created; that frame is placed on the stack. A stack is a tower of frames and we only have access to the top most one; once that frame is finished, it gets removed from the stack and we can continue with the next one. When an error occurs in one of the methods, we often get a StackTrace in Visual Studio; this is an overview of what was on the stack the moment the error occurred. Variables declared in a method usually go on the stack. Let’s use Listing 11-1 as an example to illustrate what happens.
public int Sum(int number1, int number2)
{
    int result = number1 + number2;
    return result;
}
Listing 11-1

A simple method with local variables

When calling the method, the Stack will look like Figure 11-1.
Figure 11-1

Stack when calling Sum method

Let’s expand our example.
Math math = new Math();
int result = math.Sum(5, 6);
public class Math
{
    public int Sum(int number1, int number2)
    {
        int result = number1 + number2;
        return result;
    }
}
Listing 11-2

Calling a method on a class instance

This time we are instantiating a class and calling a method on that class. That results in the memory in Figure 11-2.
Figure 11-2

Stack and heap

The class instance lives on the heap with the stack containing a pointer to the instance. The call to the class member Sum() results in a second frame on the stack where the variables live as well.

The Heap

There are two object heaps in .NET. The large object heap and the small object heap. The small object heap contains objects that are smaller than 85K in size; all the others go on the large object heap. The reason for this split is performance. Smaller objects are faster to inspect so the garbage collector works faster on the small object heap. Objects on the heap contain an address that can be used to point to this object from the stack, hence the name Pointers. What determines the size of an object is beyond the scope of this book, so we won’t go into detail here.

The Stack

The stack is used to track data from every method call. For every method a frame is created and placed on top of the stack. A frame can be visualized as a box or container, containing all objects, or pointers to those objects, the method creates or encapsulates. After a method returns, its frame is removed from the stack.

Garbage Collection

Back to the garbage collector. The garbage collector is a piece of software included in the .NET runtime that will inspect the heap for allocated objects that are no longer referenced by anyone. If it finds any, it will remove them from the heap to free up memory. This is just one place where the garbage collector works; other places are, for example, global references or CPU registers. These are called GC Roots.

The garbage collection consists of several passes. First the GC will list all GC Roots. It will then traverse all reference trees of the roots, marking the objects that still have references. A second pass will update the references to objects that will be compacted. The third pass reclaims memory from dead objects. During this phase, live objects are moved closer together to minimize fragmentation of the heap. This compacting usually only happens on the small object heap because the large object on the large object heap takes too much time to move. However, compacting can be triggered on the large object heap manually when needed.

The garbage collector runs automatically in a .NET application. There are three possible scenarios in which garbage collection is triggered:
  • Low memory. Whenever the physical memory is low, the operating system can trigger an event. The .NET runtime hooks into this event to trigger garbage collection to help restore memory.

  • Threshold on the heap is passed. Every managed heap, there is a managed heap per .NET process, has an acceptable threshold. This threshold is dynamic and can change while the process is running. Once the threshold is crossed, garbage collection is triggered.

  • GC.Collect() is triggered. System.GC is a static wrapper around the garbage collector. Its Collect method triggers garbage collection. We can call this manually for testing purposes or in very specific scenarios, but usually we do not need to worry about this

A Look at the Threadpool

The threadpool in .NET is a pool of background threads that we can schedule work on. Depending on the system your application is running on, the runtime will create a set of background workers. Should we request more threadpool threads than the amount available, it will create extra background threads and keep those alive for future use. Since the threadpool threads are background threads, they cannot keep the process alive. Once all foreground threads have exited, the application will close and all background threads will terminate.

The threadpool has been favored over creating threads manually ever since .NET 4. The main reason is performance; threadpool threads already exist; they just need to be given a unit of work, while manual threads still need to be created and that creation is an expensive operation. An easy example of using the threadpool can be created by using the Task Parallel Library, or TPL.
var strings = new List<string>();
for (int i = 0; i < 1000; i++)
{
    strings.Add($"Item {i}");
}
Parallel.ForEach(strings, _ =>
{
    Console.WriteLine(_);
    Thread.Sleep(1000);
});
Listing 11-3

Using the Task Parallel Library

We have a list of 1000 strings. Using the TPL, we can loop over this in a parallel way with Parallel.ForEach. For each item in the list, work will be scheduled on a threadpool thread. The Thread pane in Visual Studio can visualize this.
Figure 11-3

Visualizing threadpool threads

Running a foreach loop in parallel also means that the order of the outcome can be unpredictable.
Figure 11-4

Parallel ForEach output

A different way to loop over a collection in a parallel manner is using the AsParallel extension method. AsParallel is a method in the LINQ library. It returns a ParallelQuery object. By itself it does not do any parallelization; we need to execute a LINQ query on the ParallelQuery object it returns. Listing 11-4 shows how to use this method.
var strings = new List<string>();
for (int i = 0; i < 1000; i++)
{
    strings.Add($"Item {i}");
}
foreach (string item in strings.AsParallel().Select(_ => _))
{
    Console.WriteLine(item);
    Thread.Sleep(1000);
}
Listing 11-4

Using the AsParallel method

There is no major difference in using Parallel.ForEach versus AsParallel. The way to use it differs, but the results are similar.

The static ThreadPool class in .NET can tell us how many threadpool workers we can have simultaneously. Listing 11-5 shows how; Figure 11-5 shows the result on my Intel i7 device with 32GB of RAM.
ThreadPool.GetMaxThreads(out int workerthreads, out int completionports);
Console.WriteLine($"Max number of threads in the threadpool: {workerthreads}");
Console.WriteLine($"Max number of completion ports in the threadpool: {completionports }");
Listing 11-5

Listing ThreadPool information

Figure 11-5

Max number of workers in the threadpool

The threadpool consists of two types of threads: worker threads and completion ports. Completion ports are used for handling asynchronous I/O requests. Using completion ports for I/O requests can be much more performant than creating your own threads for I/O work. There are not that many cases where we want to use completion ports. The usage of these types of threads usually happens in the .NET libraries themselves; the parts that handle I/O interrupt and request.

Most of the time, it is better to use worker threads from the threadpool, but there are a few scenarios where it might be useful to create your own threads.
  • Change priority of a thread.

  • Create a foreground thread.

  • Work that takes a long time.

Threadpool threads cannot be made into foreground threads, so if that is what you need, for example, to keep the process open when the main thread exits, then you need to create your own thread. Same with thread prioritization that also requires a new thread. Long running tasks can be scheduled on threadpool threads, but the number of threads there is limited before the system starts creating new ones, which is something we want to avoid if possible because of performance reasons. So if we schedule a lot of long running tasks, we might run out of threadpool threads; for that, we might switch to creating our own threads. As always this is very dependent on your situation, so handle with care.

Async in .NET 6

Async/Await has been around for a while now in .NET, and most .NET developers should be familiar with how to use it. .NET 6 comes with a few new additions to the await/async pattern, but let’s explore the basics before we dive into the new stuff.

Await/Async

public async Task<string> FetchData()
{
    var client = new HttpClient();
    HttpResponseMessage response = await client.GetAsync("https://www.apress.com").ConfigureAwait(false);
    string html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    return html;
}
Listing 11-6

Async operation

As an example, Listing 11-6 shows a simple method that uses HttpClient to fetch a web endpoint. We notice a couple of different things; first we have marked the method as async; we can only use the await keyword in a method, lambda or anonymous method that is modified with the async keyword. Await/async does not work in synchronous functions, in unsafe contexts, or in lock statement blocks. The return type of the method is Task<string>. Task is one of the go-to return types of asynchronous methods. Task comes from the Task Parallel Library in .NET and symbolizes a unit of work and its status; whenever we await a Task we wait for its status to become complete before continuing executing the rest of the method. When we await GetAsync, for example, the method execution stops there, scheduling the rest of the method as a continuation. Once the HTTP call completes, the result is passed into the continuation and the rest of the method executes. If we decompile this using a decompiler like ILSpy, we can clearly see how the framework is introducing statemachines into our code to keep track of the state of Tasks.
.class nested private auto ansi sealed beforefieldinit '<FetchData>d__0'
        extends [System.Runtime]System.Object
        implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
{
.override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine)
Listing 11-7

State machines

Listing 11-8 shows more intermediate language; this is the part where the HTTP calls and the reading of the data happens.
// num = (<>1__state = 0);
IL_004d: ldarg.0
IL_004e: ldc.i4.0
IL_004f: dup
IL_0050: stloc.0
IL_0051: stfld int32 Foo/'<FetchData>d__0'::'<>1__state'
// <>u__1 = awaiter2;
IL_0056: ldarg.0
IL_0057: ldloc.2
IL_0058: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1/ConfiguredTaskAwaiter<class [System.Net.Http]System.Net.Http.HttpResponseMessage> Foo/'<FetchData>d__0'::'<>u__1'
// <FetchData>d__0 stateMachine = this;
IL_005d: ldarg.0
IL_005e: stloc.s 4
// <>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
IL_0060: ldarg.0
IL_0061: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> Foo/'<FetchData>d__0'::'<>t__builder'
IL_0066: ldloca.s 2
IL_0068: ldloca.s 4
IL_006a: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::AwaitUnsafeOnCompleted<valuetype [System.Runtime]System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1/ConfiguredTaskAwaiter<class [System.Net.Http]System.Net.Http.HttpResponseMessage>, class Foo/'<FetchData>d__0'>(!!0&, !!1&)
// return;
IL_006f: nop
IL_0070: leave IL_01a3
Listing 11-8

Async calls in IL

As you can tell, a lot of code is generated when using await/async. That is why I want to advise you to use this carefully; async is not always better or faster than synchronous development.

The Task object generated when awaiting an action captures the context it was called from. When you await an async method, and don’t specify ConfigureAwait(false), the method will do its work on the thread pool and switch back to the caller’s context when finished. This is exactly the behavior that you want when you request a webresult and immediately put the data into a property that is bound against, since binding happens on the UI thread. But this is not what we want when we’re executing code in a library or in a service class, so that’s where we’ll use ConfigureAwait(false).
Figure 11-6

Captured context in a Task

In ASP.NET, ever since .NET Core 3.1, we do not need to call ConfigureAwait(false) because there is no SynchronizationContext to return to. Blazor on the other hand does have a SynchronizationContext.

Listing 11-9 shows an example of where to use ConfigureAwait(false) and where not to. The example is done in a WinForms application.
private async Task FetchData()
{
    var service = new ResourcesService();
    var result = await service.FetchAllResources();
    //textblock is bound against Json
    JsonTextbox.Text = result
}
public class ResourcesService
{
    public async Task<string> FetchAllResources()
    {
        var client = RestClient.GetClientInstance();
        var result = await client.GetAsync(“/api/data”).ConfigureAwait(false);
        string json = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
        return json;
    }
}
Listing 11-9

Usage of ConfigureAwait(false)

The FetchAllResources method has two calls that are awaited and uses ConfigureAwait(false) because we do not need to switch back to the caller context. By not returning to caller context in that method, we prevent two context switches to occur.

The FetchData method doesn’t use ConfigureAwait(false) because it needs to return to the caller context. The caller context here is the UI thread. The property that the returned value is being set to will trigger a change notification, so we need to be on the UI thread.

Cancellations

In Async operations, we often make use of CancellationTokens to cancel long running tasks. These tokens are used quite a lot across the base class library as well. However, cancelling tasks does not happen that often so it would be interesting to be able to reuse CancellationTokenSource, the object that generates CancellationTokens. Up until now, we couldn’t do this safely because we couldn’t be certain that some tasks were still referencing this token. In .NET 6, CancellationTokenSource was extended with a TryReset method. Listing 11-10 shows the use of the TryReset method.
CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private void CancelButton_OnClick(object sender, EventArgs args)
{
    _cancellationTokenSource.Cancel();
}
public async Task DoWork()
{
    if (!_cancellationTokenSource.TryReset())
    {
        _cancellationTokenSource = new CancellationTokenSource();
    }
    Task<string> data = FetchData(_cancellationTokenSource.Token);
}
public async Task<string> FetchData(CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    var client = new HttpClient();
    HttpResponseMessage response = await client.GetAsync("https://www.apress.com", token).ConfigureAwait(false);
    string html = await response.Content.ReadAsStringAsync(token).ConfigureAwait(false);
    return html;
}Once a token was actually cancelled it cannot be recycled and the TryReset method will return false.
Listing 11-10

Try to reset a cancellation token

The example shown here comes from a WinForms application where we can load data and cancel it using a cancel button. When calling the DoWork method , we try to reset the CancellationTokenSource; if we don’t succeed, we instantiate a new one. The CancellationTokenSource's CancellationToken is passed to the LoadData method. LoadData checks if the token is not cancelled and uses it for loading and deserializing the data. As long as the token was not cancelled, we can keep resetting the CancellationTokenSource for reuse.

WaitAsync

In .NET 6, Microsoft is giving us more control over when to cancel or timeout asynchronous operations by adding WaitAsync methods to Task. With WaitAsync, we can specify a cancellation token or a timeout to a task.
CancellationToken token = _cancellationTokenSource.Token;
var client = new HttpClient();
Task<HttpResponseMessage> response =
client.GetAsync("https://www.apress.com", token)
      .WaitAsync(token);
await response;
Listing 11-11

WaitAsync with a cancellation token

Listing 11-12 shows the three different options.
Task<HttpResponseMessage> taskWithToken = client
      .GetAsync("https://www.apress.com", token)
      .WaitAsync(token);
Task<HttpResponseMessage> taskWithTimeout = client
      .GetAsync("https://www.apress.com", token)
      .WaitAsync(new TimeSpan(0, 0, 10));
Task<HttpResponseMessage> taskWithBoth = client
      .GetAsync("https://www.apress.com", token)
      .WaitAsync(new TimeSpan(0, 0, 10), token);
Listing 11-12

All WaitAsync overloads

Do not mistake WaitAsync with Wait. Wait is an actual blocking operation; it will block the thread until the Tasks completes and should only be used in very specific cases. WaitAsync is a way to add cancellation or timeout configuration to an asynchronous task that will run non-blocking.

Conclusion

.NET is an easy-to-use framework. It abstracts a lot of difficult concepts away from us as developers. While it does abstract these concepts away, we still have the possibility to dive deeper and actually use the more advanced concepts. We can get full control of the garbage collector and even implement our own garbage collectors should we really want to.

.NET 6 comes with big improvements on performance, on I/O-based operations, but also in general. Await/async is extended to give more fine-grained control to us developers; CancellationTokenSource is extended to allow more reuse of tokens. The examples in this chapter are just a few examples. There are some very good resources out there that dive deep into .NET.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.234.229