14. Concurrency And Asynchrony || C# 10 Flashcards

1
Q

What is ‘concurrency’?

A

In short: more than one thing happening at a time

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

What are the most common concurrency scenarios and why the concurrency is used?

A
  1. **Writing a responsive user interface **- on WPF, mobile and Window Forms apps you must run time-consuming tasks concurrently with the code that runs your user interface to mainatain responsiveness;
  2. Allowing requests to process simultaneously - on a server, client requests can arrive concurrently and so must be handled in parallel to maintain scalability (in ASP.NET and Web API this is done automatically but must be aware of it since it can affect when using static variables for caching);
  3. Parallel programming - Code that performs intensive calculations can execute faster on multicore/multiprocessor computers if the workload is divided between cores;
  4. Speculative execution - On multicore machines, you can sometimes improve performance by predicting something that might need to be done and then doing it ahead of time (e.g. running a number of different algorithms in parallel that all solve the same task. Whichever finishes first “wins” - this is effective when you can’t know ahead of time which algorithm will execute the fastest.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What does ‘multithreading’ mean?

A

Multithreading - the general mechanism by which a program can simultaneously execute code. Multithreading is supported both by the CLR and operating system and is a fundamental concept in concurrency.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

If both ‘concurrency’ and ‘multithreading’ are about executing code simultaneously - are they actually the same things?

A

In C#, concurrency and multithreading are related but distinct concepts. Let’s explore the differences between them:

  1. Concurrency:
    Concurrency is a broader concept that refers to the ability of a system to handle multiple tasks or processes simultaneously. It doesn’t necessarily mean that the tasks are executing at the same time, but rather the system can switch between tasks rapidly, giving the illusion of parallel execution. Concurrency can be achieved through various mechanisms, including multithreading, multitasking, and asynchronous programming.

Concurrency is useful for tasks that involve waiting for external resources, I/O operations, or tasks that can be performed independently of each other. It allows a program to efficiently utilize system resources and enhance responsiveness.

  1. Multithreading:
    Multithreading is a specific implementation of concurrency, where multiple threads within a single process execute simultaneously on multiple CPU cores or processor units. Each thread represents an independent sequence of instructions that can perform a separate task or part of the same task. Multithreading is a low-level programming technique that enables parallel execution of code within a single process.

C# supports multithreading through the System.Threading namespace, which provides classes and methods for managing threads and synchronizing their interactions.

Differences:

  1. Scope:
    Concurrency encompasses a broader set of techniques and patterns that allow multiple tasks to be managed effectively. It includes multithreading but also includes other methods like multitasking and asynchronous programming.
  2. Parallelism:
    Multithreading is a specific form of concurrency that enables parallelism by utilizing multiple threads to execute tasks simultaneously. Concurrency, on the other hand, doesn’t always guarantee parallel execution; it can switch between tasks rapidly, making it appear as if they are executing in parallel.
  3. Complexity:
    Multithreading is generally more complex to manage than simple concurrency. It involves handling synchronization, race conditions, and potential deadlocks, which can be challenging to debug and maintain.

In summary, concurrency is a higher-level concept that encompasses various methods for managing multiple tasks, while multithreading is a specific technique for achieving parallel execution using multiple threads within a single process in C#.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

What is a ‘thread’?

A

A thread is an execution path that can proceed independently of others.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What are single-threaded and multithreaded environments?

A

Each thread runs within an operating system process, which provides an isolated environment in which a program runs. With a single-threaded program, just one thread runs in the process’s isolated environment, and so that thread has exclusive access to it. With multithreaded program, multiple threads run in a single process, sharing the same execution environment (memory, in particular).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Why multithreading is so useful?

A

One thread can fetch data in the background, for instance, while another thread displays the data as it arrives.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

What is a ‘shared state’ in threading?

A

In the context of threading, a “shared state” refers to data or resources that are accessible and can be modified by multiple threads concurrently. When multiple threads are running simultaneously in a multithreaded application, they share the same memory space and can access and modify the same variables, objects, or other resources.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Lets assume that we have 2 threads and we want the second one to wait for the first end and only then execute itself, how we should approach this?

A

By calling method “Join” that will wait untill the thread finished and only then executing the following logic.
Example:
Thread t = new Thread(“Go”);
t.Start();
t.Join();
Console.WriteLine(“The thread has finished!”);

void Go
{
  ... // Do something
}

Usually these two processes would execute at the same time, but now only after the “Go” method ends, the Console.WriteLine() will print the text.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Thread.Sleep(0) and Thread.Yeald essentially does the same thing - pauses the current thread for specified period. What is the difference?

A

Thread.Sleep(0) gives up the thread’s current time slice immediatly, voluntarily handing over the CPU to the threads. Thread.Yield() does the same but it gives the time slice only to threads running on the same processor.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

What are Thread.Sleep and Thread.Yield additionally usefull for?

A

Sleep(0) or Yield is occationally useful in production code for advanced performance tweaks. It’s also an excellent diagnostic tool for helping to uncover thread safety issues: if inserting Thread.Yield anywhere in the code breaks the program, you almost certainly have a bug.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

What is a thread ‘blocking’?

A

A thread is deemed blocked when its execution is paused for some reason, such as when Sleeping or waiting for another to end via Join. A blocked thread immediately yields its processor time slice and for then on it consumes no processor time until its blocking condition is satisfied.

Analogy:
Imagine you are at a buffet restaurant with limited seating, and there’s a rule that only one person can go to the food counter at a time. When you arrive at the counter, you notice that someone else is already selecting their food. In this situation, you’re a “blocked thread.”

Here’s the analogy breakdown:

  1. Blocked Thread: You (the thread) have reached the food counter but cannot proceed with selecting your food because someone else (another thread) is already there, blocking your access.
  2. Yields Processor Time Slice: Instead of waiting impatiently and hogging the counter, you courteously decide to yield your turn and let the other person finish selecting their food first. You give up your processor time slice, meaning you stop using the counter and let other threads have their turn.
  3. Consumes No Processor Time: While you wait for the other person to finish and the blocking condition to be satisfied (the food counter becomes available), you do not use any more of your time or attention. You’re in a state of suspension, not consuming any processor time.
  4. Blocking Condition Satisfied: Once the person in front of you finishes selecting their food and leaves the counter, the blocking condition is satisfied (the food counter becomes available), and you can proceed with selecting your food.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

What are the I/O-bound and compute-bound operations?

A

An operation that spends the most of its time waiting for something to happen - IO-bound operation (typically involves input or output but its not a hard requirement. Example Console.ReadLine).
In contrast, an operation that spends most of its time performing CPU-intensive work is called compute-bound

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

If local variables are kept in memory stack, does that mean that with multiple threads all local variables are kept in the same memory stack?

A

No. The CLR assigns each thread its own memory stack so that local variables are kept separate.
The difference is when at least two threads uses the same variable or object (created outside of runned method) - threads share data if they have a common reference

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Consider this block of code:
~~~
class ThreadUnsafe
{
static bool _done;

static void Main()
{
  new Thread(Go).Start();
	Go();
}

static void Go()
{
  if(!_done){Console.WriteLine("This will be printed twice instead of once"); _done = true;};
} } ~~~ Since two threads are evaluating the if statement at exactly the same time as the other thread is executing the WriteLine statement before its had a chance to set `_done` to true we get unexpected second line of printed text. Can we somehow avoid this?
A

Yes, by introducing exclusive lock while reading and writing to the shared field.
~~~
class ThreadUnsafe
{
static bool _done;
static readonly object _locker = new object();

static void Main()
{
  new Thread(Go).Start();
	Go();
}

static void Go()
{
  lock(_locker)
	{
	  if(!_done){Console.WriteLine("This will be printed only once"); _done = true;};
	}
} } ~~~ When two threads simultaneously contend a lock (which can be any reference type object), one thread waits or blocks until the lock becomes available. In this case, it ensures that only one thread can enter its code block at a time, and the statement text will be printed just once, as expected
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

What are the dangers (if there are any) of using x++ expression in multithreaded program?

A

The expression x++ executes on the underlying processor as distinct read-increment-write operations. So, if two threads execute x++ at once outside of lock, the variable can end up getting incremented once rather than twice (or worse - x could be torn, ending up with a bitwise mixture of old and new content, under certain conditions).

The problem lies in the combination of both reading and writing the shared variable x within an expression that is not atomic. Let’s break it down:

Non-Atomic Expression: The expression x++ is not an atomic operation; it consists of three separate steps: reading the current value of x, incrementing it, and writing the updated value back to x. These three steps can be interrupted and interleaved by other threads.

Concurrent Access: When multiple threads execute the x++ expression simultaneously, they might read the same initial value of x before any increment is performed, even if the variable has a different value in each thread at the beginning.

Race Condition: The race condition occurs when two or more threads attempt to access and modify shared data concurrently without proper synchronization. This situation leads to unpredictable outcomes because the interleaving of read-increment-write steps can result in lost increments or incorrect final values.

So, the problem is not just about reading the same variable value in both threads. Even if the variables have different initial values, the non-atomic nature of the x++ expression can cause it to malfunction due to interleaving of steps when multiple threads access it simultaneously.

Locking is not a silver bullet for thread safety - its easy to forget to lock around accessing a field, and locking can create problems of its own (such as deadlocking)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

Will this code run as expected?
~~~
for(int i; i<10; i++)
new Thread(() => Console.Write(i)).Start();
~~~

A

No, the result might look something like this:
0223557799
The problem is that the i variable refers to the same memory location throughout the loop’s lifetime. Therefore, each thread calls Console.Write on a variable whose value can change as it is running. The solution is to use a temporary variable:
~~~
for(int i; i<10; i++)
{
int temp = i;
new Thread(() => Console.Write(i)).Start();
}
~~~
Variable temp is now local to each loop iteration. Therefore, each thread captures a different memory location and there’s no problem.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

Will this exception be handled?

try
{
  new Thread(Go);
}
catch (Exception ex)
{
  Console.Write("Oh no!");
}

void Go()
{
  throw null;
}
A

No. Here’s why:
1. The try block only surrounds the creation of the new thread, not the code inside the thread’s Go method. The exception occurs inside the Go method, which is outside the try block’s scope.

  1. When an unhandled exception occurs within a thread, it doesn’t propagate back to the thread that created it. Instead, it is treated as an unhandled exception within that specific thread.

The solution would be to wrap the inside of the Go method in the try/catch block.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

What is a centralized exception handling?

A

These are “global” exception handling events, fired after an unhandled exception via message loop. This is useful as a backstop for logging and reporting bugs (although it wont fire for unhadled exception on worker threads that you create). Handling these events prevents the program from shutting down, although you may choose to restart the app to avoid potential corruption of state that can follow or that led to the unhadled exception.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
20
Q

What are the ‘foregrounf threads’ vs ‘background threads’?

A

Foreground threads keep the application alive as long as any one of them is running, whereas background threads do not. After all foreground threads finish, the application ends, and any background threads that are still running abruptly terminate.
All newly created threads are foreground threads (unless explicitly set otherwise) and if in our one foreground thread we have another thread, then set its property IsBackground = true; the program will stop abruptly, since foreground threads does not wait for any background threads to finish:
~~~
static void Main(string[] args)
{
Thread worker = new Thread (() => Console.ReadLine());
if (args.Length > 0) worker.isBackground = true; // if this is set to true, the app stops immediatly
worker.Start();
}
~~~

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
21
Q

What is a ‘thread priority’?

A

A thread’s Priority property determines how much execution time it is allotted relative to other active threads in the OS
enum ThreadPriority = { Lowest, BelowNormal, Normal, AboveNormal, Highest};
This becomes relevant when multiple threads are simultaneously active. You need to be careful when elevating threads priority since it may starve other threads (particularly those with user interface since it can slow down the entire computer).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
22
Q

What is “signaling” in threads?

A

Its when one thread waits for receiving notification(s) from another thread.
Simpliest signalign would be calling WaitOne on a ManualResetEvent, then it blocks current thread until another thread “opens” the signal by calling Set

In the example, we start up a thread that waits on a ManualResetEvent. It remains blocked for two seconds until the main thread signals it:
~~~
var signal = new ManualResetEvent(false);

new Thread(() => {
Console.WriteLine(“Waiting for signal”);
signal.WaitOne();
signal.Dispose();
Console.WriteLine(“Got Signal!”);
}).Start();

Thread.Sleep(2000);
signal.Set(); // “open” the signal
~~~

Just for the sake of clarity, here is the order in which this code is executed:
1. New thread starts executing the code inside the anonymous method.
2. The new thread reaches the line signal.WaitOne();.
3. Since the signal is in the initial state of false, the WaitOne() call blocks the new thread, putting it in a wait state.
4. The main thread (the one running the Main method) pauses for 2 seconds due to the Thread.Sleep(2000) call.
5. After the 2-second sleep, the main thread wakes up and calls signal.Set() to open the signal (set it to true).
6. The signal is now in an open state (true).
7. The new thread continues execution and returns from the WaitOne() call, unblocking itself.
8. The new thread disposes of the signal using signal.Dispose().
9. The new thread writes “Got Signal!” to the console.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
23
Q

What is a quite common way of making an application more responsive by using threads?

A

A popular approach is to start a worker threads for time-consuming operations. The code on a worker runs a time-consuming operation and then updates the UI when complete.
However, all rich client applications have a threading model whereby UI elements and controls can be accessed only from the thread that created them (typically the main UI thread). Hence when you want to update the UI from a worker thread you must forward the request to the UI thread (the technical term is marshal).
The other example is a Single Document Interface (SDI) application, such as Microsoft Word. Each SDI window (page) have its own UI thread, in that way each window can be made more responsive with respect to the others.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
24
Q

What is a ‘synchronization contexts’?

A

To understand the concept of synchronization contexts in the System.ComponentModel namespace, let’s use an analogy:

Imagine you are a conductor leading a large orchestra performance. The orchestra represents multiple threads in a multi-threaded application, and each musician plays a specific instrument (represents a task).

  1. Musicians (Threads): In a multi-threaded application, you have different threads, each responsible for executing a specific task. These threads can perform various actions concurrently.
  2. Conductor (Synchronization Context): The synchronization context is like a conductor that coordinates the musicians (threads) during the performance. It ensures that the musicians play in harmony, follow the correct tempo, and maintain synchronization with each other.
  3. Performance (Application Execution): The application’s execution is like the orchestra performance. It involves the interaction of multiple threads performing their tasks concurrently.
  4. Sheet Music (Tasks): Each musician follows a piece of sheet music representing their task to be executed. The sheet music contains instructions on what to play and when to play it.

Now, let’s explore what the synchronization context does and why it’s important:

Synchronization contexts in the System.ComponentModel namespace are used to manage the execution flow of tasks and their synchronization in multi-threaded environments. They help ensure that tasks are executed in a controlled and coordinated manner, allowing proper communication and synchronization between threads.

  1. Flow Control: The synchronization context acts as a conductor, managing the flow of execution among threads. It ensures that tasks are executed in a particular order and that they can communicate with each other effectively.
  2. Context Preservation: Some tasks require certain execution contexts, such as UI thread context in GUI applications. The synchronization context helps preserve the necessary execution context when tasks are scheduled for execution.
  3. Thread Safety: Synchronization contexts help enforce thread safety when dealing with shared resources and UI updates. They ensure that only one thread at a time can access critical sections of code, preventing race conditions and data corruption.
  4. Task Scheduling: The synchronization context schedules tasks for execution based on their priority, availability of resources, and the context in which they need to be executed. It helps manage the execution order and avoid thread contention.

Why is it important?

The use of synchronization contexts is crucial in scenarios where you have concurrent tasks that need coordination, like in GUI applications or multi-threaded environments. Without a synchronization context, the threads might interfere with each other, leading to unpredictable and potentially incorrect behavior.

For example, in a GUI application, the synchronization context ensures that UI updates are executed on the main UI thread, preventing cross-thread violations and providing a smooth and responsive user experience.

In summary, synchronization contexts in the System.ComponentModel namespace play a vital role in managing the execution flow, preserving execution context, and ensuring thread safety in multi-threaded environments. They are essential for coordinating tasks, maintaining synchronization, and avoiding issues that arise from concurrent execution.

Example how we can post a message from a worker thread directly to UI controls:
~~~
partial class MyWindow: Window
{
SynchronizationContext _uiSyncContext;

public MyWindow()
{
  InitializeComponent();
	// Capture the synchronization context for the current UI thread:
	_uiSyncContext = SynchronizationContext.Current;
	new Thread(Work).Start();
}

void Work()
{
  Thread.Sleep(5000); // Simulate time-consuming task
	UpdateMessage("The answer");
}

void UpdateMessage(string message)
{
  // Marshal the delegate to the UI thread;
	_uiSyncContext.Post(_ => txtMessage.Text = message, null);
} } ~~~
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
25
Q

What is a ‘Thread pool’?

A

Its a pool (collection) of pre-created recyclable threads. Thread pooling is essential for efficient parallel programming and fine-grained concurrency;
it allows short operations to run without being overwhelmed with the overload of thread startup.

26
Q

What are the distinctive properties of pooled threads?

A
  1. You cannot set a Name of a pooled thread - this makes debugging a bit difficult (although you can attach a description)
  2. Pooled threads are always background threads.
  3. Blocking pooled threads can degrade performance.

Another distinctive function of pooled threads is to ensure that a temporary excess of compute-bound work does not cause CPU oversubscription.

27
Q

What is a oversubscription in threads?

A

Oversubsription is the condition of there being more active threads than CPU cores, with the OS having to time-slice threads.
Oversubsription hurts performance because time-slicing requires expensive context switches and can invalidate the CPU caches that have become essential in delivering performance to modern processors.
The CLR prevents oversubcription in the thread pol by queueing tasks and throttling their startup. It begins by running as many concurrent tasks as there are hardware cores, and then tunes the level of concurrency via a hill-climbing algorythm, continually adjusting the workload in a particular direction. If throughput improves, it continues in the same dirrection (otherwise it reverses). This ensures that it always tracks the optimal performance curve - even in the face of competing process activity on the computer.

28
Q

When talking about oversubcription in threads topic, there is a term that is often used, called ‘time-slicing’. What it is?

A

Let’s break down the concept of time-slicing with an analogy involving a group of friends playing a board game.

Imagine you have a group of friends gathered around a table playing a complex board game. Each friend takes their turn to make decisions and move their game pieces. However, there’s a rule that each player can only take a limited amount of time before they have to let the next player take their turn. This rule ensures that no single player dominates the game and that everyone gets a fair chance to participate.

Now, let’s relate this analogy to threading and time-slicing in programming:

  1. Multiple Tasks (Players): In a computer program, different tasks or threads are like the players in the board game. Each thread represents a separate sequence of instructions that needs to be executed concurrently.
  2. CPU Time (Turns): Just like each player has a limited time for their turn in the board game, each thread is allocated a small amount of CPU time to execute its instructions.
  3. Time-Slicing (Rule): Time-slicing is the technique used by the operating system to allocate CPU time to different threads. It ensures that no single thread monopolizes the CPU for an extended period, allowing fair execution of multiple threads.
  4. Context Switch (Passing the Turn): When the allocated time for a thread is up, the operating system performs a context switch, which is like the moment when a player’s turn ends and they pass the game to the next player. During a context switch, the current thread’s state is saved, and the next thread’s state is loaded so that it can continue its execution.

Now, why do we need time-slicing?

Imagine if only one thread could run at a time. If that thread gets stuck in a long-running operation (like waiting for a network response), the entire program would freeze, and no other tasks could progress. This is highly inefficient, especially on modern processors with multiple cores that can handle multiple threads simultaneously.

Time-slicing prevents this issue by ensuring that each thread gets a fair amount of CPU time. Even if one thread is waiting for something, other threads can still make progress. This parallelism improves overall system throughput and responsiveness.

However, there’s a downside to time-slicing, which is the context switch overhead. Context switches involve saving and restoring thread states, which requires extra CPU cycles. Moreover, as threads switch in and out of execution, CPU caches can become less effective, affecting performance.

To optimize performance, developers need to strike a balance between allowing enough time for each thread to make progress without causing excessive context switches. This requires careful design and consideration of the specific tasks the threads are performing.

In summary, time-slicing is like the turns in a board game, ensuring that multiple threads in a computer program get a fair chance to execute on the CPU. It promotes parallelism and responsiveness but comes with the cost of context switch overhead and potential cache inefficiencies.

29
Q

A thread is a low-low level tool for creating concurrency, so what limitations it has?

A
  • Although it easy to pass data into a thread that you start, there’s no easy way to get a ‘return value” back from a thread that you Join. You need to set up some kind of shared field. And if the operation throws an exeption, catching and propagating that exception is equally painfull.
  • You can’t tell a thread to start something else when it’s finished; instead you must Join it (blocking your own thread in process).
30
Q

Describe what is a “Task”

A

Compared to a thread, a Task is higher-level abstraction - it represents a concurrent operation that might or might not bebacked by a thread. Tasks are compositional (you can chain them together through the use of continuations). They can use the thread pool to lessen startup latency, and with a TaskCompletionSource, they can employ a callback approach that avoids threads altogether while waiting on I/O-bound operation.

31
Q

How to block a tasks until previous is completed?

A

By calling Wait():
Task task = Task.Run(() =>
{
Thread.Sleep(200);
Console.WriteLine(“Foo”);
});
Console.WriteLine(task.IsCompleted); // False
task.Wait(); // Blocks until task is complete

32
Q

How long-running tasks affect the thread pool if the tasks include ones that also use blocking?

A

Long-running tasks have the potential to impact system performance in a few ways:

  1. Resource Saturation: If you have a significant number of long-running tasks, they might occupy a significant portion of the available threads. This doesn’t mean the thread pool size has decreased, but rather that fewer threads are available for other tasks, including short-running ones.
  2. Thread Contention: Long-running tasks can lead to thread contention. Thread contention occurs when multiple tasks are competing for the same limited set of threads. This contention can lead to situations where tasks are frequently paused and resumed due to the limited number of threads, causing more frequent context switches. This can negatively impact performance.
  3. Blocked Threads: Long-running tasks that block (e.g., waiting for I/O operations or external resources) can tie up threads for extended periods. During this time, these threads are not available to execute other tasks, even if there are short-running tasks waiting for execution.
  4. Overall Throughput: Long-running tasks can reduce the overall throughput of the system. For example, if a thread pool is executing a mix of long-running and short-running tasks, the completion of short-running tasks might be delayed due to long-running tasks, leading to decreased responsiveness.

In essence, it’s not a bottleneck in the traditional sense, but rather a situation where the behavior of long-running tasks can have a cascading effect on the efficiency of the thread pool and the responsiveness of the application as a whole. To address this issue:

  • Consider isolating long-running tasks to their own set of threads or using asynchronous programming techniques to avoid blocking.
  • Tune the thread pool size and the ratio of short-running to long-running tasks to strike a balance that optimizes system throughput and responsiveness.
  • Monitor the behavior of your application to identify performance issues related to task execution and adjust your approach accordingly.

In summary, while the number of available threads in the pool doesn’t change, the impact of long-running tasks can affect the efficient use of those threads, leading to reduced throughput and responsiveness if not managed properly.

33
Q

Lets say we have a task that has a return value of ‘int’. What is the process of getting the result value if the task takes more time to be finished? (How do we know when the result is returned and the main thread can move forward?)

A

If the task hasn’t yet finished, accessing this property will block the current thread until the task finishes:
~~~
int result = task.Result; // Blocks if not already finished
Console.WriteLine(result); // 3
~~~

Task<TResult> can be thought as a “future”, in that it encapsulates a Result that becomes available later in time.

34
Q

How the exeptions are handled in Tasks?

A

Unlike with threads, tasks conveniently propagate exceptions. So, if the code in your task throws an unhadled exception, that exception is automatically rethrown to whoever calls Wait() - or accesses the Result property of a Task<TResult>

35
Q

How you can test for a faulted task without rethrowing the exception?

A

Via the IsFaulted and IsCanceled properties of the Task. If both properties return false, no error occurred; if IsCanceled is true, an OperationCancelationException was thrown for that task; if IsFaulted is true, another type of exception was thrown, and the Exception property will indicate an error.

36
Q

How to stay alerted if an unhandled silet error occurs and we want to keep a track on that?

A

We can subscribe to unobserved exceptions at a global level via the static event TaskScheduler.UnobservedTaskException; handling this event via logging the error can make good sence.

37
Q

What is an ‘awaiter’ in asynchronous functions?

A

An awaiter is any object that exposes the two methods (OnCompleted and GetResult) and a boolean property IsCompleted. There is no interface or base class to unify all of these members.

38
Q

What are the two ways to attach a continuation on a task?

A
  1. Calling GetAwaiter on the task returns an awaiter object whose OnCompleted method tells the antecedent task (the one which called it) to execute a delegate when it finishes (or faults). If an antecedent task faults, the exception is rethrown when the continuation code calls the awaiter.GetResult().
    ~~~
    var awaiter = primeNumberTask.GetAwaiter();
    awaiter.OnCompleted(() => {
    int result = awaiter.GetResult();
    Console.WriteLine(result); // Writes result
    })
    ~~~
  2. Calling the task’s ContinueWith method:
    ~~~
    primeNumberTask.ContinueWith(antecedent => {
    int result = antecedent.Result;
    Console.WriteLine(result); // Writes 123
    });
    ~~~
    ContinueWith itself returns a Task, which is useful if you want to attach further continuations. However you must deal directly with AggregateException if the task faults. In non-UI contexts, you must specify TaskContinuationOptions.ExecuteSyncronously if you want the continuation to execute on the same thread; otherwise it will bounce to the thead pool.
39
Q

What a TaskCompletionSource allows you to do?

A

TaskCompletionSource lets you create a task out of any operation that completes in the future. It works by giving you a “slave” task that you manually drive - by indicating when the operation finishes or faults. This is ideal for I/O-bound work: you get all the benefits of tasks (with their ability to propagate return values, exceptions, and continuations) without blocking a thread for the duration of the operation.
~~~
Task<TResult> Run<TResult> (Func<TResult> function)
{
var tcs = new TaskCompletionSource<TResult>();
new Thread(() =>
{
try{ tcs.SetResult(function()); }
catch(Exception ex){ tcs.SetException(ex); }
}).Start();
return tcs.Task;
}
...
Task<int> task = Run(() => { Thread.Sleep(5000); return 43;}
~~~
The real power of TaskCompletionSource is in creating tasks that don't tie up threads.</int></TResult></TResult></TResult></TResult>

40
Q

Thread.Sleep and Task.Delay does the same thing. What is the difference?

A

Thread.Sleep is a synchronous method.
Task.Delay is the asynchronous equivalent of Thread.Sleep method.

41
Q

Think of a main difference between synchronous and asynchronous methods

A

A synchronous operation does its work before returning to the caller;
An asynchronous operation an do (most or all of) its work after returning to the caller. Also asynchronous methods typically return quickly (or immediately) to the caller; thus, they are also called nonblocking methods;

42
Q

What is Asynchronous programming?

A

The principle of asynchronous programming is that you write long-running (or potentially long running) functions asynchronously. This is in contrast to the conventional approach of writing long-running functions synchronously, and then calling those functions from a new thread or task to introduce concurrency as required.

43
Q

What are the differences with asynchronous programming approach compared to conventional one?

A

The difference with the asynchronous approach is that concurrency is initiated inside the long-running function rather than from outside function.

44
Q

What are the benefits of asynchronous programming?

A
  • I/O-bound concurrency can be implemented without tying up threads, improving scalability and efficiency;
  • Rich-client applications (the ones that needs installation before running) ends up with less code on worker threads, simplifying thread safety;
45
Q

Can you name any uses for asynchronous programming based on its benefits?

A

There are two distinct benefits of asynchronous programming:
1. Writting (typically server-side) applications that deal efficiently with a lot of concurrent I/O: The challenge here is not thread-safety (because there’s usually minimal shared state) but thread efficiency; in particular, not consuming a thread per network request. So in this context, it’s only I/O-bound operations that benefit from asynchrony.
2. Simplify thread-safety in rich-client applications: This is particularly relevant as a program grows in size, becauseto deal with complexity, we typically refactor larger methods into smaller ones, resulting in chains of methods that call one another (call graphs).

46
Q

What is a coarse-grained concurrency and fine-grained concurrency in a context of asynchronous programming?

A

With a traditional synchronous all graph, if any operation within the graph is long-running, we must run the entire call graph on a worker thread to maintain a responsive UI. Hence, we end up with a single concurrent operation that spans many methods (coars-grained concurrency), and this requires considering thread-safety for every method in the graph.
With an asynchronous call graph, we need not start a thread until it’s actually needed, typically low in the graph (or not at all in the case of I/O-bound operations). All other methods can run entirely on the UI thread, with much-simplified thread safety. This returns fine-grained concurrency - a sequence of small concurrent operations, between which execution bounces to the UI thread.

47
Q

What do async and await keywords do?

A

In essence, the async and await keywords make the code cleaner and more readable by allowing you to write asynchronous code in a more sequential and natural way. They also automatically manage the continuation of asynchronous tasks, eliminating the need for explicit callback handling. This simplifies code structure, making it easier to understand and maintain while still benefiting from the responsiveness of asynchronous programming.

48
Q

What do the await keyword mean and how it is translated in compiler?

A

The await keyword simplifies the attaching of continuations. The compiler turns this:
~~~
var result = await expression;
statement(s);
~~~
Into this:
~~~
var awaiter = expression.GetAwaiter();
awaiter.OnCompleted()) =>
{
var result = awaiter.GetResult();
statement(s);
}
~~~

Upon encountering an await expression, execution (normally) returns to the caller - rather like with yield return in an iterator. But before returning, the runtime attaches a continuation to the awaited task, ensuring that when the task completes, execution jumps back into the method and continues where it left off. If the task faults, its exception is rethrown, otherwise its return value is assigned to the await expression.

49
Q

What does the async keyword mean in a compiler?

A

The async modifier instruct the compiler to treat await as a keyword rather than an identifier should an ambiguity arise within that method (this ensures that code written prior to c#5 that might use await as an identifier will still compile without error).

50
Q

Can you use async keyword in an interface?

A

Although it makes no sence to use async in an interface, it is legal, for instance, to introduce async when overriding a non-async virtual method, as long as you keep the signature the same.

51
Q

Can you assign await keyword only to a task?

A

The expression upon which you await is typically a task; however, any object with a GetAwaiter method that returns an awaiter (implementing INotifyCompletion.OnCompleted and with an appropriately typed GetResult method and a bool IsCompleted property) will satisfy the compiler.

52
Q

What happens when we have two asynchronous methods but we dont have an await keyword next to them when running them?

A

When you have two asynchronous functions that are not awaited immediately after firing them, they will start executing concurrently (in parallel) in separate threads or tasks. These tasks will run independently, and you won’t necessarily know when the results are returned unless you later await those tasks or use some other synchronization mechanism to coordinate their completion.

Here’s a simple example in C# to illustrate this:

```csharp
using System;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
Task<int> task1 = DoAsyncWork(1);
Task<int> task2 = DoAsyncWork(2);</int></int>

    // The tasks are started but not awaited immediately

    // ... Some other work can happen here concurrently ...

    // Now we await the tasks to get their results
    int result1 = await task1;
    int result2 = await task2;

    Console.WriteLine($"Result from task 1: {result1}");
    Console.WriteLine($"Result from task 2: {result2}");
}

static async Task<int> DoAsyncWork(int id)
{
    await Task.Delay(1000); // Simulate asynchronous work
    return id * 10;
} } ~~~

In this example, the DoAsyncWork method simulates asynchronous work by delaying for a second. The Main method starts two tasks (task1 and task2) representing the asynchronous work. After firing the tasks, there’s a comment indicating where other concurrent work can happen. Finally, the code awaits both tasks using await statements to retrieve their results before printing them.

If you don’t await the tasks before doing other work or exiting the method, they will continue executing independently. You won’t know when they finish unless you later await them explicitly or use synchronization mechanisms like Task.WhenAll to wait for multiple tasks to complete.

It’s important to note that launching a large number of tasks without proper management can lead to inefficiencies due to excessive context switching and resource usage. In some cases, using mechanisms like thread pools or limiting the number of concurrently executing tasks might be advisable.

53
Q

What is an asynchronous stream?

A

Asynchronous streams let you write an iterator that awaits, yielding elements asynchronously.
To generate asynchronous stream you should write a method that includes both yield return and await, and it should return IAsyncEnumerable<T> :
~~~
async IAsyncEnumerable<int> RangeAsync(int start, int count, int delay)
{
for(int i = start; i < start + count; i++)
{
await Task.Delay(delay);
yield return i;
}
}
~~~
and to use it:
~~~
await foreach( var number in RangeAsync(0, 10, 500))
{
Console.WriteLine(number);
}
~~~</int>

54
Q

What is a ‘synchronous completion’?

A

Consider this example:
~~~
static Dictionary<string, string> _cache = new Dictionary<string, string>();
async Task<string> GetWebPageAsync(string uri)
{
string html;
if (_cache.TryGetValue(uri, out html)) return html;
return _cache[uri] = await new WebClient().DownloadStringTaskAsync(uri);
}
~~~
Should a URI already exist in the cache, execution returns to the caller with no awaiting having occured, and the method returns an *already-signaled* task. When you await a synchronously completed task, execution does not return to the caller and bounce back via a continuation; instead, it proceeds immediately to the next statement. The compiler implements this optimization by checking the IsCompleted property on the awaiter.</string>

ChatGPT: In C# and programming in general, “synchronous completion” refers to the concept of executing tasks sequentially, waiting for each task to finish before proceeding to the next one. It’s like serving dishes one by one at a dinner party. In contrast, “asynchronous execution” allows tasks to run concurrently and independently, similar to how your friends can enjoy a buffet without waiting for each other.

55
Q

How the compiler optimizes the ‘await’ expression on a synchronously completed task?

A

Compiler first check if the task has already finished (the quick tasks may be already finished by the time we reach an await keyword) and if so, skips the waiting of the await expression and continues to the next code.

56
Q

Define how can we aquire progress reporting in asynchronous methods?

A

By implementing IProgress<T> interface that has only one method Report.
~~~
Task Foo(IProgress<int> onProgressPercentChanged)
{
return Task.Run(() =>
{
for(int i = 0; i<100; i++)
{
if(i % 10) onProgressPercentChanged.Report(i/10);
}
}
}
~~~</int>

57
Q

What is an cancellation pattern in asynchronous patterns?

A

it allows to cancel a concurrent operation after it’s started, perhaps in response to a user request. CLR provides a type called CancellationToken and CancellatioTokenSource. One is the token and the other type hold the method ‘Cancel()’. The separation provides some security: a method that has access only to a CancellationToken object can check for but no initiate a cancellation.
~~~
var cancelationSource = new CancellationTokenSource();
Task foo = Foo(cancelationSource.Token);
… (some time later)…
cancelationSource.Cancel();
~~~

58
Q

What is a Task-Based Asynchronous Pattern (TAP) ?

A

The Task-Based Asynchronous Pattern (TAP) in C# is a programming pattern and framework that provides a structured way to work with asynchronous operations using the Task type. TAP was introduced to make it easier for developers to write, read, and maintain asynchronous code.

TAP is built around the Task type, which represents an asynchronous operation that can produce a result or an exception. The main idea behind TAP is to provide a consistent and unified way to handle asynchronous operations, whether they are I/O-bound (such as reading/writing files or making network requests) or CPU-bound (such as heavy computations).

A TAP method does the following:
* returns a “hot” (running) Task or Task<TResult>;
* Has an ‘Async’ suffix (except for special cases such as task combinators);
* Is overloaded to accept a cancellation token and/or IProgress<T> if it supports cancellation and/or progress reporting;
* Returns quickly to the caller (has only a small initial synchronous phase);
* Does not tie up a thread if I/O-bound;</T>

59
Q

What are the “Task Combinators”?

A

They are functions that usefully combine tasks, without regard for what those specific tasks do:
* Task.WhenAny : returns a task that completes when any one of a set of tasks complete
~~~
Task<int> winningTask = await Task.WhenAny(Delay1(), Delay2(), Delay3());
~~~
Because Task.WhenAny itself returns a task, we await it, which returns the task that finished first.
* Task.WhenAll: returns a task that completes when all of the tasks that you pass to it completes.
The difference between simply running three separate asynchronous methods one by one is that should one of them faults the Task.WhenAll will run until all of the methods finishes. That means if one or two methods fail, their exceptions are combined into the task's AggregateException.</int>

60
Q

if the ValueTask acts as ordinary Task, can we use it freely as an variable type?

A

No, the ValueTask was created for performsce purposes only and has value-type semantics that can lead to incorrect behaviour
Call .AsTask() if you need to perform the following :
* Awaiting the same ValueTask multiple times;
* Calling GetAwaiter().GetResult() when the operation hasnt completed.

61
Q

What is the difference between Task<T> and ValueTask<T>?

A

ValueTask<T> is intended for micro-optimization scenarios.
If we consider that Task/Task<T> are reference types, and so instantiation requires a heap-based memory allocation and subsequent collection we would in theory want (in extreme scenarios) to have code that does not instantiate reference types and burdening garbage collection. To support this the ValueTask and ValueTask<T> structs have been introduced, which the compiler alows in place of Task/Task<T>
The important detail is that ValueTask is allocation free if the operation completes synchroniously
~~~
async ValueTask<int> Foo() {...}
int answer = await Foo(); // (Potentially) allocation-free
~~~
If the operation doesnt finish synchroniously , `ValueTask<T>` creates ordinary `Task<T>` behind the scenes and nothing is gained.
`ValueTask<T>` can also be converted to `Task<T>` by calling `AsTask` method.</T></T></T></T></int>