This section demonstrates the benefits of executing compute-intensive tasks asynchronously in a GUI app.
Figure 23.1 demonstrates executing an asynchronous task from a GUI app. Consider the GUI at the end of Fig. 23.1. In the GUI’s top half, you can enter an integer, then click Calculate to calculate that integer’s Fibonacci value (discussed momentarily) using a compute-intensive recursive implementation (Section 23.3.2). Starting with integers in the 40s (on our test computer), the recursive calculation can take seconds or even minutes to calculate. If this calculation were to be performed synchronously, the GUI would freeze for that amount of time and the user would not be able to interact with the app (as we’ll demonstrate in Fig. 23.2). We launch the calculation asynchronously and have it execute on a separate thread so the GUI remains responsive. To demonstrate this, in the GUI’s bottom half, you can click Next Number repeatedly to calculate the next Fibonacci number by simply adding the two previous numbers in the sequence. For the screen captures in Fig. 23.1, we used the top half of the GUI to calculate Fibonacci(45)
, which took over a minute on our test computer. While that calculation proceeded in a separate thread, we clicked Next Number repeatedly to demonstrate that we could still interact with the GUI and that the iterative Fibonacci calculation is much more efficient.
The powerful technique of recursion was introduced in Section 7.16. The examples in this section and in Sections 23.4–23.5 each perform a compute-intensive recursive Fibonacci calculation (defined in the Fibonacci
method at lines 53–63). The Fibonacci series
0, 1, 1, 2, 3, 5, 8, 13, 21, …
begins with 0 and 1, and each subsequent Fibonacci number is the sum of the previous two Fibonacci numbers. The Fibonacci series can be defined recursively as follows:
Fibonacci(0) = 0 Fibonacci(1) = 1 Fibonacci(n) = Fibonacci(n – 1) + Fibonacci(n – 2)
A word of caution is in order about recursive methods like the one we use here to generate Fibonacci numbers. The number of recursive calls that are required to calculate the nth Fibonacci number is on the order of 2n. This rapidly gets out of hand as n gets larger. Calculating only the 20th Fibonacci number would require on the order of 220 or about a million calls, calculating the 30th Fibonacci number would require on the order of 230 or about a billion calls, and so on. This exponential complexity can humble even the world’s most powerful computers! Calculating just Fibonacci(47) recursively—even on today’s most recent desktop and notebook computers—can take minutes.
calculateButton_Click
The Calculate button’s event handler (lines 21–36) initiates the call to method Fibonacci
in a separate thread and displays the results when the call completes. The method is declared async
(line 21) to indicate to the compiler that the method will initiate an asynchronous task and await
the results. In an async
method, you write code that looks as if it executes sequentially, and the compiler handles the complicated issues of managing asynchronous execution. This makes your code easier to write, debug, modify and maintain, and reduces errors.
Line 29 creates and starts a Task<TResult>
(namespace System.Threading.Tasks
), which promises to return a result of generic type TResult
at some point in the future. Class Task
is part of .NET’s Task Parallel Library (TPL) for parallel and asynchronous programming. The version of class Task
’s static
method Run
used in line 29 receives a Func<TResult>
delegate (delegates were introduced in Section 14.3.3) as an argument and executes a method in a separate thread. The delegate Func<TResult>
represents any method that takes no arguments and returns a result of the type specified by the TResult
type parameter. The return type of the method you pass to Run
is used by the compiler as the type argument for Run
’s Func
delegate and for the Task
that Run
returns.
Method Fibonacci
requires an argument, so line 29 passes the lambda expression
() => Fibonacci(number)
which takes no arguments—this labmda encapsulates the call to Fibonacci
with the argument number
(the value entered by the user). The lambda expression implicitly returns the Fibonacci
call’s result (a long
), so it meets the Func<TResult>
delegate’s requirements. In this example, Task
’s static
method Run
creates and returns a Task<long>
. The compiler infers the type long
from the return type of method Fibonacci
. We could declare local variable fibonacciTask
(line 29) with var
—we explicitly used the type Task<long>
for clarity, because Task
method Run
’s return type is not obvious from the call.
awaiting
the ResultNext, line 32 await
s the result of the fibonacciTask
that’s executing asynchronously. If the fibonacciTask
is already complete, execution continues with line 35. Otherwise, control returns to calculateButton_Click
’s caller (the GUI event-handling thread) until the result of the fibonacciTask
is available. This allows the GUI to remain responsive while the Task
executes. Once the Task
completes, calculateButton_Click
continues execution at line 35, which uses Task
property Result
to get the value returned by Fibonacci
and display it on asyncResultLabel
.
An async
method can perform statements between those that launch an asynchronous Task
and await
the results. In such a case, the method continues executing those statements after launching the asynchronous Task
until it reaches the await
expression.
Lines 29 and 32 can be written more concisely as
long result = await Task.Run(() => Fibonacci(number));
In this case, the await
operator unwraps and returns the Task
’s result—the long
returned by method Fibonacci
. You can then use the long
value directly without accessing the Task
’s Result
property.
When you click Next Number, the event handler nextNumberButton_Click
(lines 39–50) executes. Lines 42–45 add the previous two Fibonacci numbers stored in instance variables n1
and n2
to determine the next number in the sequence, update instance variables n1
and n2
to their new values and increment instance variable count
. Then lines 48–49 update the GUI to display the Fibonacci number that was just calculated.
The code in the Next Number event handler is performed in the GUI thread of execution that processes user interactions with controls. Handling such short computations in this thread does not cause the GUI to become unresponsive. Because the longer Fibonacci computation is performed in a separate thread, it’s possible to get the next Fibonacci number while the recursive computation is still in progress.