6

How do you set them up?

If I have the following code in a HttpModule.

public static event EventHandler<PostProcessingEventArgs> OnPostProcessing;

And in an async PostAuthorizeRequest task set up using EventHandlerTaskAsyncHelper.

// Fire the post processing event.
EventHandler<PostProcessingEventArgs> handler = OnPostProcessing;
if (handler != null)
{
    handler(this, new PostProcessingEventArgs { CachedPath = cachedPath });
}

And then tap into it using this.

ProcessingModule.OnPostProcessing += this.WritePath;    

private async void WritePath(object sender, PostProcessingEventArgs e)
{
    await Task.Factory.StartNew(() => Debug.WriteLine(e.CachedPath));
}

I get the following error.

An asynchronous module or handler completed while an asynchronous operation was still pending.

Edit

Ok so before I saw all these answers I got it to not throw the error by raising the event handler as follows.

EventHandlerTaskAsyncHelper postProcessHelper = 
new EventHandlerTaskAsyncHelper(this.PostProcessImage);

context.AddOnPostRequestHandlerExecuteAsync(postProcessHelper.BeginEventHandler,
postProcessHelper.EndEventHandler);

private Task PostProcessImage(object sender, EventArgs e)
{
    HttpContext context = ((HttpApplication)sender).Context;
    object cachedPathObject = context.Items[CachedPathKey];

    if (cachedPathObject != null)
    {
        string cachedPath = cachedPathObject.ToString();

        // Fire the post processing event.
        EventHandler<PostProcessingEventArgs> handler = OnPostProcessing;
        if (handler != null)
        {
            context.Items[CachedPathKey] = null;
            return Task.Run(() => handler(this, 
            new PostProcessingEventArgs { CachedImagePath = cachedPath }));
        }
    }

    return Task.FromResult<object>(null);
}

From what I can see below though this seems unwise.

The single purpose of this eventhandler would be to allow someone to run longer running tasks on the file like using something like jpegtran or pngout to post-process an image to further optimize it. What's the best approach for that?

James South
  • 10,147
  • 4
  • 59
  • 115

3 Answers3

4

You can add async event handlers using the AddOn* methods in the HttpApplication class. I'm sure that async void methods are not supported by all of them. Maybe by none of them.

To use those methods despite the fact that they do not directly support tasks, you need to adapt your task to be compatible with the APM pattern which ASP.NET uses here.

Maybe it is just sample code but you use of Task.Factory.StartNew is not helpful in the context of a web application.

Community
  • 1
  • 1
usr
  • 168,620
  • 35
  • 240
  • 369
  • Ok so if I used `context.AddOnPostRequestHandlerExecuteAsync` how would I ensure that I can pass the correct cachedpath to that task? Store it in context.Items ? Yeah that was just some quick sample code. `Task.Run(() => Debug.WriteLine(e.CachedPath))` is ok yeah? – James South Aug 17 '14 at 11:08
  • I don't know much about this cachedpath of yours. I don't know what to suggest and what the problem is. `Task.Run` has the same issue: It doesn't help you and adds overhead. – usr Aug 17 '14 at 13:53
1

The key is that you need to avoid async void. There are a couple of places where async void can trip you up.

You're already handling the first one correctly by using EventHandlerTaskAsyncHelper. I assume your setup code looks something like this:

public void Init(HttpApplication context)
{
  var helper = new EventHandlerTaskAsyncHelper(InvokePostAuthEvents);
  context.AddOnPostAuthorizeRequestAsync(helper.BeginEventHandler,
      helper.EndEventHandler);
}

With this kind of setup, you're avoiding an async void PostAuthorizeRequest.

The other side is when you raise the OnPostProcessing event. This is where you are having problems with async void. There are a variety of ways to raise async-aware events (I cover a number of them on my blog), but I prefer the "deferral" method which is used by WinStore apps, so it will likely be more familiar to developers.

I have a DeferralManager in my AsyncEx library that is designed to be used in your event args like this:

public class PostProcessingEventArgs
{
  private readonly DeferralManager _deferrals;

  public PostProcessingEventArgs(DeferralManager deferrals, ...)
  {
    _deferrals = deferrals;
    ...
  }

  public IDisposable GetDeferral()
  {
    return deferrals.GetDeferral();
  }

  ...
}

When you raise the event, you do this:

Task RaisePostProcessingEventAsync()
{
  EventHandler<PostProcessingEventArgs> handler = OnPostProcessing;
  if (handler == null)
    return TaskConstants.Completed;
  var deferrals = new DeferralManager();
  var args = new PostProcessingEventArgs(deferrals) { CachedPath = cachedPath };
  handler(this, args);
  return deferrals.SignalAndWaitAsync();
}

Note that raising the event is now an asynchronous operation, since it will (asynchronously) wait for all the event handler deferrals to complete.

Regular (synchronous) event handlers require no changes, but asynchronous event handlers need to use a deferral, like this:

private async void WritePath(object sender, PostProcessingEventArgs e)
{
  using (e.GetDeferral())
  {
    await Task.Delay(1000);
    Debug.WriteLine(e.CachedPath);
  }
}

As a final note, neither StartNew nor Run is a good idea on ASP.NET. If you have synchronous code to run, just run it directly.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • So at the end you point out that it's a complete waste of processing time to start a new thread to do some processing, when the perfectly good calling thread is sitting around doing nothing waiting for the "worker thread" to finish – Mick Aug 17 '14 at 13:08
  • @Mick: I was assuming that `StartNew(() => Debug.WriteLine(...))` was an example placeholder for true asynchronous work. – Stephen Cleary Aug 17 '14 at 17:21
  • That was a placeholder just to test that the event was firing. That does raise another question though. How do you run synchronous time-consuming jobs without using `Task.Run`? Say I wanted to enumerate through a series of directories to clean up files. How do I manage that without it causing request queuing and thread pool growth? – James South Aug 17 '14 at 21:34
  • The point I was trying to make has no bearing on how much work is being done in the worker thread. Spawning new threads is costly, it takes time to create them, perhaps I'm misundestanding the code but it looks like you have a thread A managed by the HttpModule, from thread A you're creating thread B to do some work, and then stopping thread A until thread B terminates. What is the point of thread B? Why not just do all the processing in Thread A? Great you're using the parallel library, but nothing is being executed in parallel. – Mick Aug 18 '14 at 01:55
  • @Mick: What I'm talking about is true asynchronous operations, which [do not use a thread](http://blog.stephencleary.com/2013/11/there-is-no-thread.html). – Stephen Cleary Aug 18 '14 at 02:06
  • 1
    @JamesSouth: Synchronous time-consuming jobs should just be run directly. Note that your example (enumerate through a series of directories to clean up files) is technically asynchronous and not synchronous, but the .NET BCL unfortunately does not provide asynchronous versions of those methods. – Stephen Cleary Aug 18 '14 at 02:09
  • @StephenCleary Perhaps you should read this... http://www.drdobbs.com/cpp/multithreaded-asynchronous-io-io-comple/201202921 "When creating a new port object, the caller simply passes INVALID_HANDLE_VALUE for the first parameter, NULL for the second and third parameters, and either zero or a positive number for the ConcurrentThreads parameter." – Mick Aug 18 '14 at 02:25
  • Perhaps they aren't threads directly managed through .NET but the underlying implementation is creating threads to process these tasks. They are not threads explicitly created for the task, but a pool from which they are processed. In any case I'd be surprised if the cost for setting all this up is zero. – Mick Aug 18 '14 at 02:27
  • And besides... the nature of this error is a bit of a hint that there actually are threads in play other than the request thread – Mick Aug 18 '14 at 02:32
  • @Mick: I know how IOCPs work -- the whole point is that they *don't block a thread* during the I/O. There is definitely a cost (mostly memory) for setting this up; one that is usually offset by reducing the number of threads running on the machine when under load. The op's error does not imply any additional threads, because "asynchronous" does *not* mean "multithreaded". – Stephen Cleary Aug 18 '14 at 03:00
  • I think I've encountered this error in a HttpModule that predates the System.Threading.Tasks namespace. – Mick Aug 18 '14 at 04:23
  • @Mick: Asynchronous support in ASP.NET dates back to at least ASP.NET 1.1 (possibly ASP.NET 1.0). `async`/`await` just makes asynchronous code easier than it used to be. – Stephen Cleary Aug 18 '14 at 12:24
  • Didn't know that! I'm curious as to what that code in pre-dating the async / await constructs looks like. – Mick Aug 18 '14 at 23:50
0

It's complaining that the worker thread hasn't completed before the request thread is terminated. This is a no no... for all it knows your worker thread might not ever terminate, which would lead to thread starvation in a very short space of time.

If you want to have a worker thread you need to create it in the Init of your HttpModule. I think this is a pretty good example...

http://msdn.microsoft.com/en-us/library/hh567803(v=cs.95).aspx

So have a single worker thread that runs for the duration of the module and and let the requests simply add work for the worker thread to process

Mick
  • 6,527
  • 4
  • 52
  • 67