Hoping to increase the speed of my WebService with Asynchronous calls, but not sure how
I am writing a WebService that consumes a third party webservice and greatly extends the functionality. For example, in one portion of the workflow I have to loop through the results of one API call, and for each result make another API call in order to return results which are actually usable. Currently this results in roughly 7,500 lines of XML, as well as a 3-4 minute load time (Granted, this load time is based upon running the WebService in debug mode from Visual Studio on a crappy PC with a crappy internet connection, and I expect it to be quite a bit snappier when run from a high-end Windows server). What I would like to do is find some way to spawn a new Asyncronous thread for each API call (so that each iteration does not have to wait for the previous iteration to finish), but I'm not sure how to do this and still have the ability to return the XML开发者_高级运维 output in the same function call. Any ideas?
::EDIT:: -- Here is the code with which I generate my XML. Note that all function calls are just wrappers to API calls for the third party API.
public List<AvailabilityList> getResortsForDate(String month, int year) {
List<RegionList> regions = this.getRegionLists( );
List<AvailabilityList> availability = new List<AvailabilityList>();
foreach(RegionList parent in regions)
{
foreach(Region child in parent.Regions)
{
if (!String.Equals(child.ID, "?"))
{
int countryID = Int32.Parse(parent.CountryID);
AvailabilityList current = this.getExchangeAvailability(countryID, month, year, child.ID);
if (current.ListCount != 0)
{
availability.Add(current);
}
}
}
}
return availability;
}
::EDIT #2:: SOLUTION! This is the solution I ended up using, which is a minor adjustment to the Answer I have chosen. Thanks! After timing my previous code (5 minutes and 1 second), this code is a huge improvement at 1 minute and 6 seconds, with 30 seconds of time belonging to another method which I will be optimizing as well!
public List<AvailabilityList> _asyncGetResortsForDate(String month, int year) {
List<RegionList> regions = this.getRegionLists();
List<AvailabilityList> availability = new List<AvailabilityList>();
List<WaitHandle> handles = new List<WaitHandle>();
List<AvailabilityList> _asyncResults = new List<AvailabilityList>();
regions.ForEach(parent => {
parent.Regions.ForEach(child => {
if (!String.Equals(child.ID, "?")) {
int countryID = Int32.Parse(parent.CountryID);
Func<AvailabilityList> _getList = () => this.getExchangeAvailability(countryID, month, year, child.ID);
IAsyncResult res = _getList.BeginInvoke(new AsyncCallback(
x => {
AvailabilityList result = (x.AsyncState as Func<AvailabilityList>).EndInvoke(x);
if (result.ListCount > 0)
{
_asyncResults.Add(result);
}
}), _getList);
while (handles.Count >= 60)
{
int item = WaitHandle.WaitAny(handles.ToArray( ));
handles.RemoveAt(item);
}
handles.Add(res.AsyncWaitHandle);
}
});
});
WaitHandle.WaitAll(handles.ToArray());
return _asyncResults;
}
Mucking about with arrays of wait handles like this is a sign that there's something entirely too complicated in your code. You can do a much cleaner job with the Task Parallel Library.
For example:
public List<AvailabilityList> _asyncGetResortsForDate(String month, int year)
{
List<RegionList> regions = this.getRegionLists();
List<AvailabilityList> availability = new List<AvailabilityList>();
List<Task> tasks = new List<Task>();
List<AvailabilityList> _asyncResults = new List<AvailabilityList>();
regions.ForEach(parent =>
{
parent.Regions.ForEach(child =>
{
if (!String.Equals(child.ID, "?"))
{
int countryID = Int32.Parse(parent.CountryID);
var childId = child.ID;
Task t = Task.Factory.StartNew((s) =>
{
var rslt = getExchangeAvailability(countryId, month, year, childId);
lock (_asyncResults)
{
_asyncResults.Add(rslt);
}
});
tasks.Add(t);
}
});
});
Task.WaitAll(tasks);
return _asyncResults;
}
(I haven't tried to compile that, but you get the gist of the idea.)
Let the TPL worry about the 64 wait handle limit.
Also note that your code had a bug just waiting to happen. Since multiple tasks could be trying to add results to the _asyncResults
list, you have to protect it with a lock. List<T>.Add
is not thread safe. If two threads try to access it concurrently, you'll end up with either corrupt data or an exception.
The above might also be faster. I'm not sure what happens if you start multiple asynchronous calls. It's likely that the thread pool will create the maximum number of threads for them, and start them all running. You could end up with 25 or more running threads with the accompanying context switches, etc. The TPL, on the other hand, is much smarter about using threads. It will create fewer concurrent threads, thus avoiding a large amount of context switching.
You can avoid the lock altogether if you use Task<List<AvailabilityList>>
. Your code then becomes something like:
Task<List<AvailabilityList>> t = Task<List<AvailabilityList>>.Factory.StartNew((s) =>
{
return getExchangeAvailability(countryId, month, year, childId);
}
And then, after your Task.WaitAll(tasks)
:
foreach (var t in tasks)
{
_asyncResults.Add(t.Result);
}
In fact, you can get rid of the Task.WaitAll(tasks)
, since Task<T>.Result
blocks until a result is available.
Here is one way to do it asynchronously, where each time you call getExchangeAvailability() it does so on a seperate thread, then waits for all threads to complete before returning the final list.
public List<AvailabilityList> _asyncGetResortsForDate(String month, int year)
{
List<RegionList> regions = this.getRegionLists();
List<AvailabilityList> availability = new List<AvailabilityList>();
List<WaitHandle> handles = new List<WaitHandle>();
List<AvailabilityList> _asyncResults = new List<AvailabilityList>();
regions.ForEach(parent =>
{
parent.Regions.ForEach(child =>
{
if (!String.Equals(child.ID, "?"))
{
int countryID = Int32.Parse(parent.CountryID);
Func<AvailabilityList> _getList = () =>
this.getExchangeAvailability(countryID, month, year, child.ID);
IAsyncResult res = _getList.BeginInvoke(new AsyncCallback(
x =>
{
AvailabilityList result =
(x.AsyncState as Func<AvailabilityList>).EndInvoke(x);
_asyncResults.Add(result);
}), _getList);
handles.Add(res.AsyncWaitHandle);
}
});
});
WaitHandle.WaitAll(handles.ToArray());
return _asyncResults;
}
Keep in mind however that if the number of iterations exceeds 64, being that the default maximum number of concurrent threads (using BeginInvoke()) is 64, you won't be processing anything asynchronously after that point until one of the 64 already running threads becomes free. There also may or may not be some overhead in making the context switches between threads. One thing you might want to check is how long each API call takes by itself to see if its really worth it.
EDIT - Concerning the 64 thread limit error, I would suggest two things,
1) You should group the asynchronous calls so that only each 'parent' is executing on its own thread, rather than every single child. That should reduce the # of threads, something like:
public List<AvailabilityList> _getAllChildren(RegionList parent, string month, int year)
{
List<AvailabilityList> list = new List<AvailabilityList>();
parent.Regions.ForEach(child =>
{
if (!String.Equals(child.ID, "?"))
{
int countryID = Int32.Parse(parent.CountryID);
AvailabilityList result = this.getExchangeAvailability(countryID, month, year, child.ID);
list.Add(result);
}
});
return list;
}
public List<AvailabilityList> _asyncGetResortsForDate(String month, int year)
{
List<RegionList> regions = this.getRegionLists();
List<AvailabilityList> availability = new List<AvailabilityList>();
List<WaitHandle> handles = new List<WaitHandle>();
List<AvailabilityList> _asyncResults = new List<AvailabilityList>();
regions.ForEach(parent =>
{
Func<List<AvailabilityList>> allChildren = () => _getAllChildren(parent, month, year);
IAsyncResult res = allChildren.BeginInvoke(new AsyncCallback(
x =>
{
List<AvailabilityList> result =
(x.AsyncState as Func<List<AvailabilityList>>).EndInvoke(x);
_asyncResults.AddRange(result);
}), allChildren);
handles.Add(res.AsyncWaitHandle);
});
WaitHandle.WaitAll(handles.ToArray());
return _asyncResults;
}
2) You might need to add a check before adding to the List of WaitHandles to see if you are at risk of exceeding 64 threads:
var asyncHandle = res.AsyncWaitHandle;
if (handles.Count >= 64)
asyncHandle.WaitOne(); // wait for this one now
else if (handles.Count < 64)
handles.Add(asyncHandle);
精彩评论