In C #, The iterator is used to process waiting tasks,

Source: Internet
Author: User

In C #, The iterator is used to process waiting tasks,

Introduction

Maybe you have read C #5 about async and await keywords and how they help simplify asynchronous programming. Unfortunately, just two years after VS2010 is upgraded, ren ran is not ready to upgrade to VS2012. asynchronous keywords cannot be used in VS2010 and C #4. You may think, "If I can write a seemingly synchronous method in VS 2010, but asynchronous execution. my code will be clearer."

After reading this article, you will be able to do this. We will develop a small infrastructure code so that we can write the "synchronous method that looks like synchronization, but asynchronous execution" method. This VS2012 asynchronous keyword is the same and enjoy the features of C #5.

We must admit that async and await are very good syntax sugar, and our method needs to write more "AsyncResultcallback" methods to adapt to this change. when you finally upgrade to VS2012 (or later), this will be a trivial matter. replace this method with the C # keyword, as long as the simple syntax changes, instead of a hard structure rewrite.

Summary

Async/await is a keyword Based on the asynchronous Task Mode. I will not describe it here because there is a very complete document description. But it must be pointed out that TAP is very handsome! With this, you can create a large number of small unit jobs (tasks) that will be completed at a certain time in the future. Tasks can start other (nested) jobs) task and/or create subsequent tasks that will be started only after the pre-job is completed. The relationship between the frontend and subsequent tasks can be one-to-many or multiple-to-one. When embedded tasks are completed, parent tasks do not need to be associated with threads (heavyweight Resources !) Bound. When executing a task, you don't have to worry about the timing of the thread. You just need to make a few tips and the framework will automatically handle these tasks for you. When the program starts to run, all the tasks will go to the end as a stream flows into the sea, and they will rebound and interact with each other like Bai qingge's iron ball.

However, in C #4, we do not have async or await, but what is missing is only a little bit. new Features of Net5, which can be avoided or built by ourselves. The key Task types are still available.


In a C #5 asynchronous (async) method, you have to wait for a Task. This will not lead to thread waiting. Instead, this method returns a Task to its caller, which can wait (if it is asynchronous) or attach Subsequent parts. (It can also call Wait () in the task or its results, but this will be coupled with the thread, so avoid doing that .) When the waiting task is completed successfully, your Asynchronous Method will continue to run where it is interrupted.

You may know that the C #5 compiler will rewrite its Asynchronous Method to generate a nested class that implements the state machine. C # exactly has a feature (starting from 2.0): iterator (yield return method ). The method here is to use an iterator method to build a state machine in C #4 and return a series of tasks waiting for steps in the entire processing process. We can write a method to receive an enumeration of tasks returned from the iterator, and return an overloaded Task to represent the completion of all sequences and provide its final results (if any ).

Final Goal

Stephen Covey suggested that we have a sequence of goals. This is what we are doing now. There are already a lot of examples to show us how to use async/await to implement SLAMs (synchronous-looking asynchronous methods ). Therefore, we do not use these keywords to implement this function. Let's take a C #5 async example to see how to implement it in C #4. Then we will discuss the general method to convert these codes.

The following example shows how to implement the asynchronous read/write method Stream. CopyToAsync () in C #5. Assume that this method is not implemented in. net5.
 

public static async Task CopyToAsync(  this Stream input, Stream output,  CancellationToken cancellationToken = default(CancellationToken)){  byte[] buffer = new byte[0x1000];  // 4 KiB  while (true) {    cancellationToken.ThrowIfCancellationRequested();    int bytesRead = await input.ReadAsync(buffer, 0, buffer.Length);    if (bytesRead == 0) break;     cancellationToken.ThrowIfCancellationRequested();    await output.WriteAsync(buffer, 0, bytesRead);  }}


For C #4, we will divide it into two parts: one is the method with the same access capability, and the other is the private method. The parameter is the same but the return type is different. The private method implements the same processing through iteration, and the result is a series of waiting tasks (IEnumerable <Task> ). The actual tasks in a sequence can be non-generic or any combination of different types of generics. (Fortunately, the generic Task <T> type is a subtype of the non-generic Task Type)

The same access capability (public) method returns the same type as the corresponding async method: void, Task, or generic Task <T>. It calls the private iterator using the extension method and converts it to a Task or Task <T>.
 

public static /*async*/ Task CopyToAsync(  this Stream input, Stream output,  CancellationToken cancellationToken = default(CancellationToken)){  return CopyToAsyncTasks(input, output, cancellationToken).ToTask();}private static IEnumerable<Task> CopyToAsyncTasks(  Stream input, Stream output,  CancellationToken cancellationToken){  byte[] buffer = new byte[0x1000];  // 4 KiB  while (true) {    cancellationToken.ThrowIfCancellationRequested();    var bytesReadTask = input.ReadAsync(buffer, 0, buffer.Length);    yield return bytesReadTask;    if (bytesReadTask.Result == 0) break;     cancellationToken.ThrowIfCancellationRequested();    yield return output.WriteAsync(buffer, 0, bytesReadTask.Result);  }}

The Asynchronous Method is usually named after "Async" (unless it is an event processor such as startButton_Click ). Give the iterator the same name and "Tasks" (for example, startButton_ClickTasks ). If the Asynchronous Method returns the void value, it still calls ToTask () but does not return the Task. If the Asynchronous Method returns the Task <X>, it calls the General ToTask <X> () Extension Method. There are three types of responses. The asynchronous method can be used as follows:
 

public /*async*/ void DoSomethingAsync() {  DoSomethingAsyncTasks().ToTask();}public /*async*/ Task DoSomethingAsync() {  return DoSomethingAsyncTasks().ToTask();}public /*async*/ Task<String> DoSomethingAsync() {  return DoSomethingAsyncTasks().ToTask<String>();}

The paired iterator method is not more complex. When the Asynchronous Method waits for a non-general Task, the iterator simply transfers control to it. When the Asynchronous Method waits for the task result, the iterator saves the task in a variable, transfers it to the method, and then uses its return value. Both cases are shown in the CopyToAsyncTasks () example above.

For a SLAM that contains general resultTask <X>, the iterator must forward the control to the exact type. ToTask <X> () converts the final task to that type to extract the result. Often, your iterator calculates the result value from the intermediate task, and you only need to package it in the Task <T> .. NET 5 provides a convenient static method. No. NET 4, so we use TaskEx. FromResult <T> (value) to implement it.

The last thing you need to know is how to handle the value returned in the middle. An asynchronous method can be returned from multiple nested blocks. Our iterator simply imitates it by jumping to the end.

 

// C#5public async Task<String> DoSomethingAsync() {  while (…) {    foreach (…) {      return "Result";    }  }} // C#4; DoSomethingAsync() is necessary but omitted here.private IEnumerable<Task> DoSomethingAsyncTasks() {  while (…) {    foreach (…) {      yield return TaskEx.FromResult("Result");      goto END;    }  }END: ;}

Now we know how to write SLAM in C #4, but only FromResult <T> () and two ToTask () extension methods can be implemented. Let's start.

Simple start

We will implement three methods in the class System. Threading. Tasks. TaskEx, starting with the two simple methods. The FromResult () method first creates a TaskCompletionSource (), assigns a value to its result, and returns the Task.
 

public static Task<TResult> FromResult<TResult>(TResult resultValue) {  var completionSource = new TaskCompletionSource<TResult>();  completionSource.SetResult(resultValue);  return completionSource.Task;}

Obviously, the two ToTask () methods are basically the same. The only difference is whether to assign values to the Result attribute of the returned object Task. generally, we do not write two pieces of the same Code, so we will use one of the methods to implement the other. We often use generics as the returned result set, so that we do not need to care about the return value, but also avoid type conversion at the end. Next we will first implement the method that does not use generics.
 

private abstract class VoidResult { } public static Task ToTask(this IEnumerable<Task> tasks) {  return ToTask<VoidResult>(tasks);}

So far, the ToTask <T> () method has not been implemented.

The first naive attempt

For the implementation method we tried for the first time, we will enumerate the Wait () of each task to complete, and then use the final task as the result (if appropriate ). Of course, we don't want to occupy the current thread. We will execute another thread to loop this task.
 

// BAD CODE !public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks){  var tcs = new TaskCompletionSource<TResult>();  Task.Factory.StartNew(() => {    Task last = null;    try {      foreach (var task in tasks) {        last = task;        task.Wait();      }       // Set the result from the last task returned, unless no result is requested.      tcs.SetResult(        last == null || typeof(TResult) == typeof(VoidResult)          ? default(TResult) : ((Task<TResult>) last).Result);     } catch (AggregateException aggrEx) {      // If task.Wait() threw an exception it will be wrapped in an Aggregate; unwrap it.      if (aggrEx.InnerExceptions.Count != 1) tcs.SetException(aggrEx);      else if (aggrEx.InnerException is OperationCanceledException) tcs.SetCanceled();      else tcs.SetException(aggrEx.InnerException);    } catch (OperationCanceledException cancEx) {      tcs.SetCanceled();    } catch (Exception ex) {      tcs.SetException(ex);    }  });  return tcs.Task;}


There are some good things here. In fact, it is really useful, as long as it does not touch the user interface:
It accurately returns a TaskCompletionSource Task and sets the completion status through the source code.

  • It shows how to set the final Result of a task through the last task of the iterator, and avoid the possibility of no Result.
  • It captures exceptions from the iterator and sets the Canceled or Faulted status. it also transmits the enumerated task status (Wait (), which may throw a set of exceptions that wrap cancellation or fault ).

But there are some major problems here. The most serious problems are:

  • Since the iterator needs to implement the promise of "Asynchronous State", when it is initialized from a UI thread, the iterator method will be able to access the UI control. You can find that the foreach loop is running in the background. Do not touch the UI at that time! This method does not take SynchronizationContext into account.
  • We are also in trouble outside the UI. We may want to create a large number of parallel Tasks implemented by SLAM. But look at the Wait () in the loop ()! When waiting for a nested task, it may take a long time to complete remotely. We will suspend a thread. We are faced with the depletion of thread Resources in the thread pool.
  • This method of uninstalling Aggregate exceptions is not natural. We need to capture and spread its completion state without throwing an exception.
  • Sometimes SLAM can determine its completion status immediately. In that case, C #5 async can be asynchronous and effective. Here we always plan a background task, so we lose that possibility.

It's time to find a solution!

Continuous Loop

The biggest idea is to obtain the first task generated by the iterator directly. We have created a continuation so that it can check the status of the task upon completion, and (if successful) can receive the next task and create another continuation until it ends. (If not, the iterator does not need to be completed .)

 

// It's awesome, but we haven't. Public static Task <TResult> ToTask <TResult> (this IEnumerable <Task> tasks) {var taskScheduler = SynchronizationContext. Current = null? Taskschedation. Default: taskschedcontext. FromCurrentSynchronizationContext (); var tcs = new TaskCompletionSource <TResult> (); var taskEnumerator = tasks. GetEnumerator (); if (! TaskEnumerator. moveNext () {tcs. setResult (default (TResult); return tcs. task;} taskEnumerator. current. continueWith (t => ToTaskDoOneStep (taskEnumerator, taskschedstep, tcs, t), taskScheduler); return tcs. task;} private static void ToTaskDoOneStep <TResult> (IEnumerator <Task> taskEnumerator, TaskScheduler taskschedsource, TaskCompletionSource <TResult> tcs, Task completedTask) {var status = completedTa Sk. status; if (status = TaskStatus. canceled) {tcs. setCanceled ();} else if (status = TaskStatus. faulted) {tcs. setException (completedTask. exception);} else if (! TaskEnumerator. MoveNext () {// sets the result returned by the final task until no result is needed. Tcs. SetResult (typeof (TResult) = typeof (VoidResult )? Default (TResult): (Task <TResult>) completedTask ). result);} else {taskEnumerator. current. continueWith (t => ToTaskDoOneStep (taskEnumerator, taskschedstep, tcs, t), taskScheduler );}}

There are many worth sharing:


Our subsequent sections (continuations) Use TaskScheduler that involves SynchronizationContext, if any. This allows our iterator to be called immediately after the UI thread initialization or at a continue point to access the UI control.
The process is not interrupted, so there is no thread pending! By the way, the call to itself in ToTaskDoOneStep () is not a recursive call; it is in taskEnumerator. the anonymous function called after Currenttask ends. The current activity exits almost immediately after calling ContinueWith (). It is completely independent of the subsequent part.
In addition, we verify the status of each nested task in the continue point, instead of checking a predicted value.


However, there must be at least one major problem and a few minor problems.

If the iterator throws an unprocessed exception or throws OperationCanceledException and cancels it, we do not process it or set the status of the main task. This is what we have done before, but this version is lost.
In order to fix Issue 1, we have to introduce the same exception handling mechanism to call MoveNext () in both methods. Even now, both methods have the same subsequent part. We violate the creed of "do not repeat yourself.

What if the Asynchronous Method is expected to give a result, but the iterator does not provide it and exits? Or is its last task of the wrong type? In the first case, we silently return the default result type. In the second case, we throw an unprocessed InvalidCastException, and the main task will never end! Our program will be permanently suspended.

Finally, what if a nested task is canceled or an error occurs? We set the status of the master task and will no longer call the iterator. It may be in a using block or inside a try block with finally, and some cleanup is required. We should follow the process to end it when it is interrupted, rather than wait for the garbage collector to do this. How can we do this? Of course, you can use a later part!

To solve these problems, we moved the MoveNext () call from ToTask () and replaced it with a synchronous call to initialize ToTaskDoOneStep. Then we will add appropriate exception handling in a precaution.

Final Version

Here is the final implementation of ToTask <T>. it uses a TaskCompletionSource to return to the main task, which will never cause thread waiting. If some words still involve SynchronizationContext, The iterator will handle exceptions and directly spread the completion of nested tasks (instead of aggresponexception ), when appropriate, a value is returned to the main task. When a result is expected and the SLAM iterator does not end with the correct genericTask <T> type, an error is returned with a friendly exception.
 

public static Task<TResult> ToTask<TResult>(this IEnumerable<Task> tasks) {  var taskScheduler =    SynchronizationContext.Current == null      ? TaskScheduler.Default : TaskScheduler.FromCurrentSynchronizationContext();  var taskEnumerator = tasks.GetEnumerator();  var completionSource = new TaskCompletionSource<TResult>();   // Clean up the enumerator when the task completes.  completionSource.Task.ContinueWith(t => taskEnumerator.Dispose(), taskScheduler);   ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, null);  return completionSource.Task;} private static void ToTaskDoOneStep<TResult>(  IEnumerator<Task> taskEnumerator, TaskScheduler taskScheduler,  TaskCompletionSource<TResult> completionSource, Task completedTask){  // Check status of previous nested task (if any), and stop if Canceled or Faulted.  TaskStatus status;  if (completedTask == null) {    // This is the first task from the iterator; skip status check.  } else if ((status = completedTask.Status) == TaskStatus.Canceled) {    completionSource.SetCanceled();    return;  } else if (status == TaskStatus.Faulted) {    completionSource.SetException(completedTask.Exception);    return;  }   // Find the next Task in the iterator; handle cancellation and other exceptions.  Boolean haveMore;  try {    haveMore = taskEnumerator.MoveNext();   } catch (OperationCanceledException cancExc) {    completionSource.SetCanceled();    return;  } catch (Exception exc) {    completionSource.SetException(exc);    return;  }   if (!haveMore) {    // No more tasks; set the result (if any) from the last completed task (if any).    // We know it's not Canceled or Faulted because we checked at the start of this method.    if (typeof(TResult) == typeof(VoidResult)) {    // No result      completionSource.SetResult(default(TResult));     } else if (!(completedTask is Task<TResult>)) {   // Wrong result      completionSource.SetException(new InvalidOperationException(        "Asynchronous iterator " + taskEnumerator +          " requires a final result task of type " + typeof(Task<TResult>).FullName +          (completedTask == null ? ", but none was provided." :            "; the actual task type was " + completedTask.GetType().FullName)));     } else {      completionSource.SetResult(((Task<TResult>) completedTask).Result);    }   } else {    // When the nested task completes, continue by performing this function again.    taskEnumerator.Current.ContinueWith(      nextTask => ToTaskDoOneStep(taskEnumerator, taskScheduler, completionSource, nextTask),      taskScheduler);  }}

Look! Now you will use C #4 (or VB10) without async and await in Visual Studio 2010 to write SLAMs (seemingly synchronous method, but asynchronous execution ).

Interesting

Until the last version, I have been passing a CancellationTokenUp to ToTask () and spreading it to ToTaskDoOneStep () in the subsequent sections (). (This is irrelevant to this article, So I removed them. You can see the annotated trace in the sample code .) There are two reasons for this. First, when processing OperationCanceledException, I will check its CancellationToken to confirm that it matches this operation. If not, it will use an error to replace the cancellation action. Although technically correct, it is unfortunate that the cancellation token may be confused and it is not worthwhile to pass irrelevant information between the call passed to ToTask () and the subsequent part. (If you Task experts can give me a good case that can be confirmed in the comment, I will reconsider)

The second reason is that I will check whether the token is canceled. before every MoveNext () call the iterator, I will immediately cancel the master task and exit the process. This allows you to cancel the token without going through the iterator. I don't think this is the right thing to do (because it is inappropriate to cancel an asynchronous process at yield return) -- It is more likely that it is completely under the control of the iterator process -- but I want to try it. It cannot work. I found that in some cases, the task will be canceled, but the subsequent part will not be triggered. See the sample code. The resume button is available, but it does not, so the button is still unavailable after the process ends. In the sample code, I leave the commented out method for canceling the token. You can put the method parameter for canceling the token back and test it. (I am very grateful if your Task experts can explain why this is the case !)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.