Leveraging multi-threading when calling Web Services or SQL Server
If you need to make more than one call to one or more web services in your code in a single request, then you can do this using multiple threads for vastly better performance.
There are three ways of solving this problem, depending upon which layer your code resides (UI, Business Layer or Data Layer).
If your code is in the UI layer, then you can make use of the Async parameter in your ASPX:
<%@ Page Language="C#" Async="true" CompileWith="SlowWSAsync.aspx.cs" ClassName="Whatever.SlowWSAsync_aspx" %> |
like this (SlowWsAsync.aspx.cs):
… using System.Threading; namespace Chevron.UpstreamArchitecture.Services.Whatever { public partial class SlowWSAsync_aspx { // Get an instance of our Slow Web Service Slow slowWebservice = new Slow(); void Page_Load(object sender, EventArgs e) { BeginEventHandler bh = new BeginEventHandler(this.BeginGetAsyncData); EndEventHandler eh = new EndEventHandler(this.EndGetAsyncData); AddOnPreRenderCompleteAsync(bh, eh); }
IAsyncResult BeginGetAsyncData(Object src, EventArgs args, AsyncCallback cb, Object state) { // Note - this is serviced on the same thread as Page_Load // but a different thread is used to service EndGetAsyncData // return slowWebservice.DoWhatever(cb, state); }
void EndGetAsyncData(IAsyncResult ar) { string ret = slowWebservice.DoWhatever (ar); } } } |
If you’re in the Business layer, then you can use something like this:
… using System.Threading; namespace Chevron.UpstreamArchitecture.Services.Whatever { class ConsoleApp { static void Main(string[] args) { DoStuffSimultaneously doit = new DoStuffSimultaneously(); // Call our method, passing in the data we want it to work on in parallel. string stringsAppendedInMulitpleThreads = doit.DoStuff(new string[] { "one ", "two ", "three " }); // Spit out the results Console.WriteLine(stringsAppendedInMulitpleThreads); } } class DoStuffSimultaneously { string _response = string.Empty; // Common object the threads update with their work. AutoResetEvent[] _waitAllEvents; // Array of objects to wait upon. /// <summary> /// This method takes in a set of work and calls multiple threads for each unit of work. /// </summary> /// <param name="whatToDo">An array of strings to be appended together, each by a different thread.</param> /// <returns>The result of all the multiple threads' work.</returns> public string DoStuff(string[] whatToDo) { // Create an array of objects to wait upon; we need one per thread _waitAllEvents = new AutoResetEvent[whatToDo.Count];
// Populate array of waiting objects, one for each work item for (int i = 0; i < whatToDo.Count; i++) _waitAllEvents[i] = new AutoResetEvent(false); // Create callback on the method we want to do the work WaitCallback callBack = new WaitCallback(CallSlowWebServiceInNewThread); // Iterate through all our work items, creating a thread for each and passing in the data // we want the thread to work on and the event we are waiting on as a state object (a Pair in this example - could be any object) for(int i=0; i < whatToDo.Count; i++) ThreadPool.QueueUserWorkItem(callBack, new Pair(whatToDo[i], _waitAllEvents[i])); // Wait until all our threads have signaled their wait object is done. if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) { // WaitAll for multiple handles on an STA thread is not supported. // ...so wait on each handle individually. foreach (WaitHandle myWaitHandle in _waitAllEvents) WaitHandle.WaitAny(new WaitHandle[] { myWaitHandle }); } else { WaitHandle.WaitAll(_waitAllEvents); } // When we get here then all our threads have done their work, so we can return our result object. return _response; } /// <summary> /// This method calls our slow web service does some work in a separate thread. /// </summary> /// <param name="state">A Pair object containing a string as the work item, and a AutoResetEvent to set when we are done.</param> static void CallSlowWebServiceInNewThread(object state) { // Get the work for this thread out of the pair Pair pair = state as Pair; // Get an instance of our Slow Web Service Slow slowWebservice = new Slow(); // Call slow Web Service using our work item (be careful of race conditions here; so lock work item) lock (_response) { // Get something from our Slow WebService _response += slowWebservice.DoSomething(pair.First); }
// Get our wait item out of the pair AutoResetEvent autoResetEvent = pair.Second as AutoResetEvent; // Set our wait item to indicate we are done here. autoResetEvent.Set(); } } } |
Finally, if your code is in the Data Layer and you’re calling directly against SQL Server with a bunch of slow queries then you can use something like the code example below to have SQL Server work on them simultaneously and tell us when it’s done with them.
… using System.Threading; namespace Chevron.UpstreamArchitecture.Services.Whatever { class ExecSimultAsyncSqlStatementWaitAll { // Command-line app to demo launching loads of *simultaneous* SQL commands and // waiting for them all to come back, where we can deal with all them in one loop. static void Main(string[] args) { // Note "Asynchronous Processing=true" at the end of the connection string... string sqlConnectString = "Data Source=whatever;Integrated Security=SSPI;" + "Initial Catalog=whatever;Asynchronous Processing=true"; // This is just used to make SQL Server wait a random few seconds to simulate a long query Random rnd = new Random((int)DateTime.Now.Ticks); // Create an array of SQL commands with "n" members to simulate a batch of work to do int n = 10; SqlConnection[] connection = new SqlConnection[n]; SqlCommand[] command = new SqlCommand[n]; string[] sqlSelect = new string[n]; IAsyncResult[] asyncResult = new IAsyncResult[n]; WaitHandle[] _waitAllEvents = new WaitHandle[n]; // This object is what we use to wait on the batch // Kick off 10 select statements that will run simultaneously; we DON'T wait for each one here for (int i = 0; i < n; i++) { // In this example, each command actually just waits for between 1 and 10 random seconds // In reality this could just be a slow query. sqlSelect[i] = "WAITFOR DELAY '00:00:" + rnd.Next(1, 10) + "';"; connection[i] = new SqlConnection(sqlConnectString); connection[i].Open(); command[i] = new SqlCommand(sqlSelect[i], connection[i]); // Kick off a call to SQL Server; it will return *instantly* with an object we can wait upon asyncResult[i] = command[i].BeginExecuteNonQuery(); Console.WriteLine("[{0}] Command {1} started: {2}", DateTime.Now, i, sqlSelect[i]); // capture something to wait on for this command wh[i] = asyncResult[i].AsyncWaitHandle; } // Wait for all processes to complete and output results if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) { // WaitAll for multiple handles on an STA thread is not supported. // ...so wait on each handle individually. foreach (WaitHandle myWaitHandle in _waitAllEvents) WaitHandle.WaitAny(new WaitHandle[] { myWaitHandle }); } else { WaitHandle.WaitAll(_waitAllEvents); } for (int i = 0; i < wh.Length; i++) { int recAff = command[i].EndExecuteNonQuery(asyncResult[i]); // Get back the results of each query Console.WriteLine("[{0}] Command {1} completed, records affected = {2}", DateTime.Now, i, recAff); connection[i].Close(); } Console.WriteLine("\nPress any key to continue."); Console.ReadKey(); } } } |
Comments
- Anonymous
October 15, 2008
/me waves at Adam! "Hello from Austin" :-)