23.5 Asynchronous Execution of Two Compute-Intensive Tasks

When you run any program, its tasks compete for processor time with the operating system, other programs and other activities that the operating system is running on your behalf. When you execute the next example, the time to perform the Fibonacci calculations can vary based on your computer’s processor speed, number of cores and what else is running on your computer. It’s like a drive to the supermarket—the time it takes can vary based on traffic conditions, weather, timing of traffic lights and other factors.

Figure 23.3 also uses the recursive Fibonacci method, but the two initial calls to Fibonacci execute in separate threads. The first two outputs show the results on a dual-core computer. Though execution times varied, the total time to perform both Fibonacci calculations (in our tests) was typically significantly less than the total sequential-execution time in Fig. 23.2. Dividing the compute-intensive calculations into threads and running them on a dual-core system does not perform the calculations twice as fast, but they’ll typically run faster than when performed in sequence on one core. Though the total time was the compute time for the longer calculation, this is not always the case, as there’s overhead inherent in using threads to perform separate Tasks.

The last two outputs show that executing calculations in multiple threads on a single-core processor can actually take longer than simply performing them synchronously, due to the overhead of sharing one processor among the app’s threads, all the other apps executing on the computer and the chores the operating system was performing.

Fig. 23.3 Fibonacci calculations performed in separate threads.

Alternate View

   1   // Fig.  23.3: AsynchronousTestForm.cs
   2   // Fibonacci calculations performed in separate threads
   3   using System;
   4   using System.Threading.Tasks;
   5   using System.Windows.Forms;
   6
   7   namespace FibonacciAsynchronous
   8   {
   9      public partial class AsynchronousTestForm : Form
  10     {
  11        public AsynchronousTestForm()
  12        {
  13           InitializeComponent();
  14        }
  15
  16        // start asynchronous calls to Fibonacci
  17        private async void startButton_Click(object sender, EventArgs e)
  18        {
  19           outputTextBox.Text =
  20              "Starting Task to calculate Fibonacci(46)
";
  21
  22           // create Task to perform Fibonacci(46) calculation in a thread
  23           Task<TimeData> task1 = Task.Run(() => StartFibonacci(46));  
  24
  25           outputTextBox.AppendText(
  26              "Starting Task to calculate Fibonacci(45)
");
  27             
  28           // create Task to perform Fibonacci(45) calculation in a thread
  29           Task<TimeData> task2 =  Task.Run(() => StartFibonacci(45));
  30          
  31           await Task.WhenAll(task1, task2); // wait for both to complete
  32
  33           // determine time that first thread started
  34           DateTime startTime =
  35              (task1.Result.StartTime < task2.Result.StartTime) ?
  36              task1.Result.StartTime : task2.Result.StartTime;
  37
  38           // determine time that last thread ended
  39           DateTime endTime =
  40              (task1.Result.EndTime < task2.Result.EndTime) ?
  41              task1.Result.EndTime : task2.Result.EndTime;
  42
  43          // display total time for calculations
  44	    double totalMinutes = (endTime - startTime).TotalMinutes;
  45          outputTextBox.AppendText(
  46	       $"Total calculation time = {totalMinutes:F6} minutes
");
  47        }	
  48				
  49	   //	starts a call to Fibonacci and captures start/end times
  50	   TimeData StartFibonacci(int n)
  51	   {	
  52	      // create a TimeData object to store start/end times
  53	      var result = new TimeData();
  54				
  55	      AppendText($"Calculating Fibonacci({n})");
  56	      result.StartTime = DateTime.Now;
  57	      long fibonacciValue = Fibonacci(n);
  58	      result.EndTime = DateTime.Now;
  59				
  60	      AppendText($"Fibonacci({n}) = {fibonacciValue}");
  61	      double minutes =
  62		 (result.EndTime - result.StartTime).TotalMinutes;
  63	      AppendText($"Calculation time = {minutes:F6} minutes
");
  64			
  65	      return result;
  66	  }	
  67				
  68 	 // Recursively calculates Fibonacci numbers
  69	 public	long Fibonacci(long n)
  70	 {	
  71	    if (n ==  0|| n == 1)
  72	    {
  73	        return n;
  74	    }
  75	    else
  76	    {
  77	        return Fibonacci(n - 1) + Fibonacci(n - 2);
  78	    }
  79	 }	
  80				
  81	 // append text to outputTextBox in UI thread
  82	 public	void AppendText(String text)
  83	 {	
  84	    if (InvokeRequired) // not GUI thread, so add to GUI thread
  85	     {
  86	        Invoke(new MethodInvoker(() => AppendText(text)));
  87	     }
  88	     else // GUI thread so append text
  89	     {
  90	       outputTextBox.AppendText(text + "
");
  91	     }
  92	  }	
  93     }		
  94   }			

23.5.1 awaiting Multiple Tasks with Task Method WhenAll

In method startButton_Click, lines 23 and 29 use Task method Run to create and start Tasks that execute method StartFibonacci (lines 50–66)—one to calculate Fibonacci(46) and one to calculate Fibonacci(45). To show the total calculation time, the app must wait for both Tasks to complete before executing lines 34–46. You can wait for multiple Tasks to complete by awaiting the result of Task static method WhenAll (line 31), which returns a Task that waits for all of WhenAll’s argument Tasks to complete and places all the results in an array. In this app, the Task’s Result is a TimeData[], because both of WhenAll’s argument Tasks execute methods that return TimeData objects. Class TimeData is defined as follows in this project’s TimeData.cs file:


class TimeData
{
   public DateTime StartTime { get; set; }
   public DateTime EndTime { get; set; }
}

We use objects of this class to store the time just before and immediately after the call to Fibonacci—we use these properties to perform time calculations. The TimeData array can be used to iterate through the results of the awaited Tasks. In this example, we have only two Tasks, so we interact with the task1 and task2 objects directly in the remainder of the event handler.

23.5.2 Method StartFibonacci

Method StartFibonacci (lines 50–66) specifies the task to perform—in this case, to call Fibonacci (line 57) to perform the recursive calculation, to time the calculation (lines 56 and 58), to display the calculation’s result (line 60) and to display the time the calculation took (lines 61–63). The method returns a TimeData object that contains the time before and after each thread’s call to Fibonacci.

23.5.3 Modifying a GUI from a Separate Thread

Lines 55, 60 and 63 in StartFibonacci call method AppendText (lines 82–92) to append text to the outputTextBox. GUI controls are designed to be manipulated only by the GUI thread—all GUI event handlers are invoked in the GUI thread automatically. Modifying GUI controls one from a non-GUI thread can corrupt the GUI, making it unreadable or unusable. When updating a control from a non-GUI thread, you must schedule that update to be performed by the GUI thread. To do so in Windows Forms, check the inherited Form property InvokeRequired (line 84). If this property is true, the code is executing in a non-GUI thread and must not update the GUI directly. Instead, you call the inherited Form method Invoke method (line 86), which receives as an argument a Delegate representing the update to perform in the GUI thread. In this example, we pass a MethodInvoker (namespace System.Windows.Forms)—a Delegate that invokes a method with no arguments and a void return type. We initialize the MethodInvoker with a lambda expression that calls AppendText. Line 86 schedules this MethodInvoker to execute in the GUI thread. When the method is called from the GUI thread, line 90 updates the outputTextBox. Similar concepts also apply to WPF and Universal Windows Platform GUIs.

23.5.4 awaiting One of Several Tasks with Task Method WhenAny

Similar to WhenAll, class Task also provides static method WhenAny, which enables you to wait for any one of several Tasks specified as arguments to complete. WhenAny returns the Task that completes first. One use of WhenAny might be to initiate several Tasks that perform the same complex calculation on computers around the Internet, then wait for any one of those computers to send results back. This would allow you to take advantage of computing power that’s available to you to get the result as fast as possible. In this case, it’s up to you to decide whether to cancel the remaining Tasks or allow them to continue executing. For details on how to do this, see

https://msdn.microsoft.com/library/jj155758

Another use of WhenAny might be to download several large files—one per Task. In this case, you might want all the results eventually, but you’d like to start processing immediately the results from the first Task that returns. You could then call WhenAny again for the remaining Tasks that are still executing.

..................Content has been hidden....................

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