WCF REST Not Processing Asynchronously

2020-03-02 06:06发布

问题:

We are currently implementing a new WCF REST service in IIS for our site and on a number of pages we may be making a handful of AJAX calls using JQuery asynchronously. The problem is that it seems as though WCF (on the server side) is executing synchronously.

On page load we're making 3 separate calls to 3 different methods. Using logging, I can see them all hit the global.asax file within about 5ms of each other. From there, the logging shows everything executing in the order they exit the global.asax (not necessarily the order in which we made the calls from the page via javascript). I expected each call to receive their own thread and return individually. Even when attaching with the debugger I can see that it won't execute the next method until I step through the current method it's on.

Here are the operation contracts for three of the methods I 'thought' I implemented to use the async model.

    [OperationContract(AsyncPattern = true)]
    [WebInvoke(
        Method = "POST"
         , UriTemplate = "/ListUserPreferences"
        , BodyStyle = WebMessageBodyStyle.Wrapped
        , ResponseFormat = WebMessageFormat.Json
        , RequestFormat = WebMessageFormat.Json
    )]
    IAsyncResult BeginListUserPreferences(AsyncCallback callback, object state);
    Result<List<Data.EnumerationItem<UserPreferenceType>>> EndListUserPreferences(IAsyncResult asyncResult);

    [OperationContract(Name = "GetUserSecure", AsyncPattern = true)]
    [WebInvoke(
        Method = "POST"
         , UriTemplate = "/GetUser"
        , BodyStyle = WebMessageBodyStyle.Wrapped
        , ResponseFormat = WebMessageFormat.Json
        , RequestFormat = WebMessageFormat.Json
    )]
    IAsyncResult BeginGetUser(AsyncCallback callback, object state);
    Result<Data.User> EndGetUser(IAsyncResult asyncResult);

    [OperationContract(AsyncPattern = true)]
    [WebInvoke(
        Method = "POST"
         , UriTemplate = "/ListWithAttributes"
        , BodyStyle = WebMessageBodyStyle.Wrapped
        , ResponseFormat = WebMessageFormat.Json
        , RequestFormat = WebMessageFormat.Json
    )]
    IAsyncResult BeginListWithAttributes(int index, int pageSize, AsyncCallback callback, object state);
    Result<PagedCollection<Data.Attribute>> EndListWithAttributes(IAsyncResult asyncResult);

Here is an example of one of the implementations in the service.

    public IAsyncResult BeginGetUser(AsyncCallback callback, object state)
    {
        var asyncResult = new CompletedAsyncResult<Result<Data.User>>(state);
        asyncResult.Result = new Result<Data.User>();

        asyncResult.Result.Value.UserId = Guid.Empty;
        asyncResult.Result.Value.DisplayName = "asdfasd";
        asyncResult.IsCompleted = true;           

        callback(asyncResult);

        return asyncResult;
    }

    public Result<Data.User> EndGetUser(IAsyncResult asyncResult)
    {
        return ((CompletedAsyncResult<Result<Data.User>>)asyncResult).Result;
    }

Here are the attributes we have on the service implementation class.

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Multiple)]

Can anyone please provide some insight as to why these are executing synchronously and what I need to do, or at least point me in the direction of what I need to do, to get these to execute asynchronously?

UPDATE

I took some of Matt's answer and moved my logic to the End function calls and followed this blog post on how he did it a bit more closely Uploading Large Files To Self Hosted WCF Rest Service. However, I couldn't get the End methods to call using his technique. I'm also starting to think I'm going about this the wrong way. Because looking at the logs, my custom authentication service is being executed before each service call, which happens before any async method operations even fire. After further investigation, I took a look at the ThreadId's of each request coming into IIS and then executing the operations. It appears that WCF is only using 1 worker thread... period. It doesn't matter how many requests I send at a time to IIS. For example, if I send 3 requests, I can see they all come in at different times (within milliseconds of each other) and all get their own thread. But then it seems WCF just queues them all and executes them in that order because they're all executed on the same thread, including the authentication service calls.

回答1:

It looks to me, from your example, like you're doing all the work before the "Begin" call finishes; that's a common mistake I found while learning the async pattern.

Though I'm not familiar with its application in WCF, the Async model more typically is a Begin method that launches some new thread with a IAsyncResult object to be updated by that new thread. To "complete" the action, when IsCompleted is set to true, the original caller is expected to pass the original IAsyncResult object back to the corresponding End method which returns the result. A trivial implementation looks as follows:

    static Func<string> getUser;
    public static IAsyncResult BeginGetUser(AsyncCallback callback, object state)
    {
        getUser = () =>
            {
                Thread.Sleep(2000);
                return "finished";
            };
        return getUser.BeginInvoke(callback, state);
    }

    public static string EndGetUser(IAsyncResult asyncResult)
    {
        return getUser.EndInvoke(asyncResult);
    }

The calls to it might look like:

var result = BeginGetUser(null, null);
string value = EndGetUser(result);

Of course, this is a trivial case: to quote http://kennyw.com/work/indigo/258, "If you aren’t doing something that’s "natively async", then you shouldn’t be using AsyncPattern=true".

Fortunately, with C# 5.0 or the Async CTP that Microsoft released, the .Net async pattern may become a thing of the past.



回答2:

To make things asynchronous you generally compose it together with other things that are asynchronous. If you are only doing synchronous things with your asynchronous methods then it doesn't make any sense to use the async pattern. For example, if all your code does is put work on the thread pool you haven't accomplished anything because you've handed back the thread to the thread pool by moving your code asynchronous but stolen it right back from ASP.NET by launching your work there.

If you are making a database call to get the user, and your database supports asynchronous operations, you might build something like this:

public IAsyncResult BeginGetUser(AsyncCallback callback, object state)
{
  var taskFunc = Task<DbReader>.Factory.FromAsync(db.BeginGetUser, db.EndGetUser);
  return taskFunc.ContinueWith(task => {
    var reader = task.Result;
    reader.Read();
    return new Data.User {
      DisplayName = reader["displayName"] as string,
      UserId = Guid.Parse(reader["userId"] as string),
    }
   }
  );
}

public Result<Data.User> EndGetUser(IAsyncResult asyncResult)
{
  return (Task<User>)(asyncResult).Result;
}

Let me stress something: Async programming is tricky. With Task it gets a little easier, but you still have to wrap your head around continuations. Debugging is a chore and one wrong move and your code just disappears and you don't know why. If you miss an exception it will appear on the finalizer thread and you'll not necessarily know why. Also, in order to do async programming you have to tip-toe into multi-threaded programming which is full of danger.

Make sure you really understand what is going on before attempting to go Asynchronous. If your code is synchronous by nature then making it asynchronous doesn't buy you very much unless it's a long-running process and you are OK spinning up another thread to handle it. People use asynchronous handlers so that IIS (or whatever underlying web server) can have its thread back to service other requests; as I mentioned before, if you then just go take a thread off the thread pool you are stealing that thread back from IIS and you won't see any gains in scalability. If you just wildly create new threads, however, you open yourself up to denial of service attacks.