Using iterators in C # to process wait Tasks _ basics

Source: Internet
Author: User
Tags exception handling visual studio 2010

Introduced

Maybe you've read c#5 about async and await keywords and how they help simplify asynchronous programming, but unfortunately, just two years after upgrading VS2010, you're not ready to upgrade to VS2012, you can't use asynchronous keywords in VS2010 and c#4, you might want to " If I can write a method that looks synchronized in VS 2010, but executes asynchronously. My code will be clearer. "

After reading this article, you will be able to do this. We will develop a small infrastructure code that allows us to write the "seemingly synchronized method, but execute asynchronously" method, which, like the VS2012 asynchronous keyword, enjoys the characteristics of c#5.

We have to admit that async and await are very good grammatical sugars, and our approach requires writing more "Asyncresultcallback" method to adapt to this change. And when you finally upgrade to VS2012 (or later), it would be a trivial matter to replace this method with the C # keyword, as long as the simple syntax changes, not a hard structure rewrite.

Profile

Async/await is a keyword based on the asynchronous task pattern. Given the very complete documentation described here, I'm not going to explain it here. But it must be pointed out that tap is absolutely awesome! With it you can create a lot of small unit work (tasks) that will be done at some point in the future, tasks that can start other (nested) tasks, and/or create some successor tasks that will not start until the current task completes. Predecessor and successor tasks can be linked to one-to-many or many-to-many relationships. When the inline task completes, the parent task does not need to be associated with the thread (Heavyweight resource!). ) binding. You don't have to worry about the timing of a thread when you perform a task, just make a few small hints and the framework will automatically handle these things for you. When the program starts to run, all the tasks will be like streams into the sea as each to the end, and like pachinko small iron ball with each other rebound interaction.

However, in c#4 we have no async and await, but the lack of it is just a little bit. The new features of NET5, these new features we can either avoid a little bit, or we can build ourselves, the key task type is still available.


In a c#5 asynchronous (async) method, you have to wait for a task. This does not cause the thread to wait; Instead, the method returns a task to its caller, which can wait (if it is asynchronous) or attach a subsequent part. (It can also call wait () in the task or its results, but this is coupled to the thread, so avoid doing that.) When the waiting task completes successfully, your asynchronous method will continue to run where it is interrupted.

Perhaps you will know that the C#5 compiler overrides its asynchronous method for a generated nested class that implements the state machine. C # has exactly one feature (starting from 2.0): iterators (the way yield return). The approach here is to use an iterator method to build the state machine in the c#4, returning a sequence of tasks that await steps in the whole process. We can write a method that receives an enumeration of the tasks returned from the iterator, returns an overloaded task to represent the completion of the entire sequence, and provides its final result (if any).

Final goal

Stephen Covey suggested we have a goal in succession. That's what we're doing now. There are plenty of examples to show us how to use async/await to implement slams (synchronous-looking asynchronous methods). So we don't use these keywords to implement this feature. Let's do a c#5 async example and see how to implement it in c#4. Then we'll discuss the general method of converting the code.

The following example shows the way in which we implement the asynchronous read-write Method Stream.copytoasync () in c#5. Suppose this method does not. NET5 in the implementation.

public static Async Task Copytoasync (this
  stream input, stream output,
  cancellationtoken CancellationToken = def Ault (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'll split into two blocks: one for the same access, the other for the private method, and the same for parameters but different return types. Private methods implement the same processing with iterations, resulting in 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 a non-generic task type)

The same access capability (public) method returns the type consistent with the corresponding Async method: Void,task, or generic task<t>. It will use the extension method to invoke the private iterator and transform it into a task or task<t>.

 public static/*async*/Task Copytoasync (this stream input, stream output, Cancel Lationtoken CancellationToken = Default (CancellationToken)) {return copytoasynctasks (input, output, CancellationToken ).
Totask (); private static ienumerable<task> Copytoasynctasks (stream input, stream output, CancellationToken cancellation  Token) {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); }
}

Asynchronous methods are usually named "Async" (unless it is an event handler such as Startbutton_click). Give the iterator the same name followed by "Tasks" (such as Startbutton_clicktasks). If the asynchronous method returns a void value, it still invokes Totask () but does not return a task. If the asynchronous method returns TASK<X>, it invokes the generic totask<x> () extension method. For three return types, the asynchronous alternative is as follows:

Public/*async*/void Dosomethingasync () {
  dosomethingasynctasks (). Totask ();
}
Public/*async*/Task Dosomethingasync () {return
  dosomethingasynctasks (). Totask ();
}
Public/*async*/task<string> Dosomethingasync () {return
  dosomethingasynctasks (). Totask<string> ();
}

A pair of iterator methods will not be more complex. When an asynchronous method waits for a task that is not universal, the iterator simply transfers control to it. When an asynchronous method waits for a task result, the iterator saves the task in a variable, moves to the method, and then uses its return value. Both cases are shown in the Copytoasynctasks () example above.

For slam that contain universal resulttask<x>, the iterator must pass control to the exact type. Totask<x> () Converts the final task to that type in order to extract its results. Often your iterator calculates the result values from the intermediate task and only needs to package it in the task<t>.. NET 5 provides a convenient static method for this. and. NET 4 does not, 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, and our iterator simply imitates it by jumping to the end.


C#5 public
Async task<string> Dosomethingasync () {while
  (...) {
    foreach (...) {return
      ' result ';
 
}}} c#4; Dosomethingasync () is the 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 are implemented to truly do so. Let's get down to it.

A simple beginning.

We will implement 3 methods under Class System.Threading.Tasks.TaskEx, starting with the simple 2 methods. The Fromresult () method creates a TaskCompletionSource (), assigns a value to its result, and returns a task.

public static task<tresult> fromresult<tresult> (TResult resultvalue) {
  var completionsource = new Taskcompletionsource<tresult> ();
  Completionsource.setresult (resultvalue);
  return completionsource.task;
}

Obviously, these 2 totask () methods are basically the same, and the only difference is whether the result property of the returned object task is assigned a value. Usually we don't write 2 of the same code, so we'll use one of the methods to implement the other. We often use generics as a return result set, so that we don't have to care about the return value at the same time to avoid the final type conversion. Let's go ahead and implement the method that doesn't have a generic type.

Private abstract class Voidresult {} public
 
static Task Totask (this ienumerable<task> tasks) {return
  Totas K<voidresult> (tasks);

So far we have left a totask<t> () method has not been implemented.

The first time naïve to try

For the first time we try to implement the method, we will enumerate each task's wait () to complete, and then the final task as the result (if appropriate). Of course, we do not want to occupy the current thread, and we will execute another thread to loop the task.

Bad CODE! public static task<tresult> totask<tresult> (this ienumerable<task> tasks) {var TCS = new Taskcompleti
  Onsource<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. requested. Tcs. Setresult (last = = NULL | | typeof (TResult) = = typeof (Voidresult)? Default (TResult): (task<tresult& gt;).
 
    result); A catch (AggregateException Aggrex) {//If task. Wait () threw an exception it would be wrapped in a Aggregate;
      Unwrap it. if (AggrEx.InnerExceptions.Count!= 1) TCS.
      SetException (Aggrex); else if (Aggrex.innerexception is operationcanceledexception) TCS.
      Setcanceled (); Else TCS.
    SetException (aggrex.innerexception); The catch (OperationCanceledException Cancex) {TCS.
    Setcanceled (); catch (Exception ex) {TCS.
    SetException (ex);
  }
  }); Return TCS.
Task;
 }


Here are some good things, in fact it really works, as long as you don't touch the user interface:
It accurately returns a TaskCompletionSource task and sets the completion state through the source code.

    • It shows how we can set the final result of a task through the last task of the iterator, while avoiding situations that might not be possible.
    • It captures the exception from the iterator and sets the canceled or faulted state. It also propagates the task state of the enumeration (here is through Wait (), which may throw a collection of exceptions wrapped cancellation or fault).

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

    • Because iterators need to implement the promise of "asynchronous state", when it is initialized from a UI thread, the iterator's method will be able to access the UI control. You can see that the Foreach Loop here is running in the background; don't touch ui! from that moment on. This approach does not take into account SynchronizationContext.
    • We also have 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, and we will suspend a thread. We are facing the thread pool exhaustion of the threads resource.
    • It is not natural to aggregate the method of solving the packet anomaly. We need to capture and propagate its completion state without throwing an exception.
    • Sometimes the slam can determine its completion status immediately. In that case, the c#5 async can operate asynchronously and efficiently. Here we always plan a background task, so we lose that possibility.

It's time to think of something!

Continuous cycle

The biggest idea is to get the first task that it produces directly from the iterator. We create a continuation that enables it to check the status of the task at completion and, if successful, to receive the next task and create another continuation until it ends. (if not, the iterator has no requirements to complete.) )


It's great, but we haven't. public static task<tresult> totask<tresult> (this ienumerable<task> tasks) {var TaskScheduler = Sy Nchronizationcontext.current = null?
  TaskScheduler.Default:TaskScheduler.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, TaskScheduler, TCS, T), Tasksch
  Eduler); Return TCS.
Task; private static void Totaskdoonestep<tresult> (Ienumerator<task> taskenumerator, TaskScheduler
  TaskScheduler, taskcompletionsource<tresult> TCS, Task completedtask) {var status = Completedtask.status; if (status = = taskstatus.canceled) {TCS.
 
  Setcanceled (); else if (status = = taskstatus.faulted) {TCS.
 
  SetException (completedtask.exception); } elseif (!taskenumerator.movenext ()) {//Set the result returned by the last task until no results are required. Tcs. Setresult (typeof (TResult) = = typeof (Voidresult)? Default (TResult): ((task<tresult>) completedtask).
 
  result);
      else {taskEnumerator.Current.ContinueWith (t => totaskdoonestep (Taskenumerator, TaskScheduler, TCS, T),
  TaskScheduler);
 }
}

Here are a lot to share:


Our subsequent section (continuations) uses the taskscheduler involved in SynchronizationContext, if any. This allows our iterator to access the UI control immediately or at a continuation point after the UI thread is initialized.
The process is not interrupted, so no thread hangs up and waits! By the way, the call to itself in Totaskdoonestep () is not a recursive call; it is an anonymous function that is invoked at the end of Taskenumerator.currenttask, and the current activity exits almost immediately at the call ContinueWith (). It is completely independent of the subsequent part.
In addition, we verify the state of each nested task in the Continue point, not check for a predictive value.


However, there is at least one big problem and some smaller problems.

If the iterator throws an unhandled exception, or throws a operationcanceledexception and cancels, we do not process it or set the state of the main task. This is something we've done before but this version is missing.
To fix problem 1, we had to introduce the same exception handling mechanism in the two methods where MoveNext () was invoked. Even now, all two methods have the same set of subsequent parts. We violate the "Don't repeat Yourself" creed.

What happens if the asynchronous method is expected to give a result, but the iterator quits without providing it? Or is the last task the wrong type? In the first case, we silently return the default result type; In the second case, we throw an unhandled InvalidCastException, and the main task never reaches the end state! Our program will be permanently suspended.

Finally, what if a nested task is canceled or an error occurs? We set the main task state and never call the iterator again. It might be inside a using block, or with a finally try block, and there's some cleanup to do. We should follow the process to end it when it is interrupted and not wait for the garbage collector to do it. How do we do that? Of course through a follow-up part!

To solve these problems, we remove the MoveNext () call from Totask () instead of a synchronous call to initialize the Totaskdoonestep (). Then we will be wary of adding appropriate exception handling in one.

Final version

This is the final implementation of totask<t> (). It returns the main task with a taskcompletionsource, never causing the thread to wait, and if so, involves SynchronizationContext, which handles the exception by the iterator, Propagate the end of a nested task directly (instead of AggregateException), returning a value to the main task when it is appropriate, when expecting a result and the slam iterator does not end with the correct generictask<t> type, With a friendly exception to the error.

public static task<tresult> totask<tresult> (this ienumerable<task> tasks) {var TaskScheduler = S Ynchronizationcontext.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 taskSchedu Ler, taskcompletionsource<tresult> Completionsource, Task completedtask) {//Check status of previous nested TA
  SK (if any), and stop if Canceled or Faulted.
  TaskStatus status; if (Completedtask = = null) {//This is the ' the ' ' iteratorKip 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 the.
 
    if (typeof (TResult) = = typeof (Voidresult)) {//No result Completionsource.setresult (default (TResult)); Or else if (! Completedtask is task<tresult>)} {//Wrong result complEtionsource.setexception (New InvalidOperationException ("Asynchronous iterator" + Taskenumerator + "re Quires 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 write slams in Visual Studio 2010 with c#4 (or VB10) without async and await (the method that appears to be synchronized, but executes asynchronously).

Interesting place.

Until the last version, I have been passing a cancellationtokenup to Totask () and propagating it into the totaskdoonestep () of the subsequent section. (This is irrelevant to this article, so I removed them.) You can see the traces in the sample code that are commented out. There are two reasons for this. First, when dealing with operationcanceledexception, I check its cancellationtoken to make sure it matches this operation. If not, it will replace the cancel action with an error. Although technically true, it is unfortunate that the cancellation token may be confusing, and that the extraneous information passed to the Totask () call and subsequent portions makes it unworthy. (If you task experts can give me a good use case that can be validated in the comments, I'll reconsider)

The second reason is that I will check if the token is canceled, cancel the main task immediately before MoveNext () calls the iterator, and exit the process. This allows you to check the token without the iterator and have the cancellation behavior. I don't think it's the right thing to do (because it's not appropriate to cancel an asynchronous process at yield return)--more likely it's completely under the control of the iterator process--but I want to try. It doesn't work. I have found that in some cases the task will be canceled and the subsequent ones will not be triggered. Take a look at the sample code; Continue to perform to restore the button is available, but it does not occur so the button is not available after the process has finished. I left an annotated cancellation check in the sample code; You can put the cancellation token's method parameter back and test it. (If your task expert can explain why this is the case, I would appreciate it!) )

Related Article

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.