I have a simple .net 4.5 webforms project that I am using to output the results of some service checks. I have already created a service checking classes that are multi-threaded and awaitable (each check might take 1-3 seconds and I want them checked in parallel). I want the result of each service check to be written to the web page and flushed as soon as it is determined (I don't care if the results are in order). The service checker methods have been tested and work fine in a console application, but I'm having trouble porting it to a webforms app.
The code below somewhat works (very randomly). Sometimes, it's perfect. Other times, it "skips" displaying the results of one of the tests. Other times, it mixes the display of the CSS style in with the results! Mostly, though, it crashes on the Response.Flush line in CheckService stating "Overlapped I/O operation is in progress" and then it crashes.
How can I write the the web page response buffer when each check finishes and displays their results in a stable manner?
Here's the code for the aspx page...
<%# Page Language="C#" Async="true" AutoEventWireup="true" Inherits="ServiceMonitor._Default" Codebehind="Default.aspx.cs" %>
And here's the code behind...
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("<style type=\"text/css\">body {font-family: Arial;font-size: 10pt;}</style>");
Response.Write("Beginning processing...<br/>");
Response.Flush();
RegisterAsyncTask(new PageAsyncTask(CheckServices));
}
protected async Task CheckServices()
{
Response.Write(string.Format("Starting checks at {0}<br/>", DateTime.Now.ToLongTimeString()));
var tasks = new List<Task<ServiceStatus>>
{
Task.Run(() => CheckService("ServiceOne")),
Task.Run(() => CheckService("ServiceTwo")),
Task.Run(() => CheckService("ServiceThree"))
};
var checkResults = (await Task.WhenAll(tasks)).ToList();
Response.Write(string.Format("Checking complete at {0}<br/>", DateTime.Now.ToLongTimeString()));
Response.Flush();
}
public ServiceStatus CheckService(string serviceName)
{
var startTime = DateTime.Now;
// Simulate a longer running process, by pausing for 1-3 seconds
var random = new Random();
var end = DateTime.Now + TimeSpan.FromMilliseconds(random.Next(1000, 3000));
while (DateTime.Now < end) { }
var elapsedTime = (DateTime.Now - startTime).TotalSeconds;
var service = new ServiceStatus {Name = serviceName, IsRunning = true};
Response.Write(string.Format("Done with {0} in {1} seconds<br/>", service.Name, elapsedTime.ToString("N2")));
Response.Flush();
return service;
}
}
public class ServiceStatus
{
public string Name { get; set; }
public bool IsRunning { get; set; }
}
FYI - The app isn't supposed to be pretty, hence the removal of standard HTML markup. It also won't be accessed by many users (really just me) so IIS blocking isn't really a concern and the page should return back within 20-30 seconds.
Your problem is that you're trying to access the request context (i.e., Reponse.Write) from a random background thread (i.e., Task.Run).
The ideal solution is to go async all the way. By this, I mean to make your service checkers asynchronous but not multithreaded. If they're implemented using an API ping, then you can check out HttpClient for the implementation.
Once they're asynchronous, the CheckService method will be async as well:
public async Task<ServiceStatus> CheckServiceAsync(string serviceName)
{
var startTime = DateTime.Now;
// Simulate a longer running process, by pausing for 1-3 seconds
var random = new Random();
var waitTime = TimeSpan.FromMilliseconds(random.Next(1000, 3000));
await Task.Delay(waitTime);
var elapsedTime = (DateTime.Now - startTime).TotalSeconds;
var service = new ServiceStatus {Name = serviceName, IsRunning = true};
Response.Write(string.Format("Done with {0} in {1} seconds<br/>", service.Name, elapsedTime.ToString("N2")));
Response.Flush();
return service;
}
And it can be called concurrently, without multithreading:
protected async Task CheckServicesAsync()
{
Response.Write(string.Format("Starting checks at {0}<br/>", DateTime.Now.ToLongTimeString()));
var tasks = new List<Task<ServiceStatus>>
{
CheckService("ServiceOne"),
CheckService("ServiceTwo"),
CheckService("ServiceThree")
};
var checkResults = (await Task.WhenAll(tasks)).ToList();
Response.Write(string.Format("Checking complete at {0}<br/>", DateTime.Now.ToLongTimeString()));
Response.Flush();
}
Related
I'm having an issue with HTTP request cancellations in a blazor server app. I have a web app that uses Google Maps with custom tile overlays. The map will request 256x256 tiles from my server as the user zooms around. As the user zooms around, Google Maps, appropriately, cancels any pending HTTP requests that are no longer needed. If, however, the user moves around quickly, a lot of cancellations happen. Something in the server is getting delayed because of all the cancellations.
To attempt to debug this, I made a simple console app that uses Task.Delay taking a cancellation token.
With relatively few tasks going (2x # of logical cores)), things work as expected. As the number of tasks increases, the delay becomes extreme.
I've made a GitHub repo demonstrating all of this at: https://github.com/TNT0305/TestWait
Near the top of main, there are some configuration parameters:
induceDelay - set to true to increase the number of tasks to manifest the issue (default to false)
taskDelayMs - number of milliseconds to Task.Delay in each call (defaults to 20000ms)
On my machine, with induceDelay set to true, I get 512 calls to the task. After 100ms, I call Cancel() on the cancellation token source. It takes 18.4 seconds for the call to Cancel() to return.
I think I must be missing something. Any ideas?
Clarification
The issues are not, necessarily, how long it takes for .Cancel() to return. It has to do with how long it takes for the cancel to be detected in the async task after the .Cancel() call was initiated.
Here's the Program.cs from the GitHub repo:
using System.Text;
namespace testWait
{
/// <summary>
/// Class to record information on timing of tasks
/// </summary>
class Event
{
public int Id { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public bool CancelledBeforeStart { get; set; } = false;
public DateTime? CancelTriggerTime { get; set; }
public DateTime? CancelExceptionTime { get; set; }
// Properties to make sense of the recorded times
public double TotalDuration { get => (EndTime - StartTime).TotalSeconds; }
public double TriggeredAfter { get => CancelTriggerTime != null ? (CancelTriggerTime.Value - StartTime).TotalSeconds : -1.0; }
public double ExceptionAfter { get => CancelExceptionTime != null ? (CancelExceptionTime.Value - StartTime).TotalSeconds : -1.0; }
public override string ToString() => $"{Id},{StartTime},{TotalDuration},{TriggeredAfter},{ExceptionAfter},{CancelledBeforeStart}";
public string ReportCancel(DateTime cancelTime)
{
var t = CancelTriggerTime != null ? (CancelTriggerTime.Value - cancelTime).TotalSeconds : -1.0;
return $"{Id} cancel delegate called {t}s after cts.Cancel() was called";
}
}
internal class Program
{
static async Task Main(string[] args)
{
//////////////////////////////////////////////////////
// RUN CONFIGURATION
// set induceIssue to true to observe excessive delay. Set to false to observe expected behaviors
bool induceIssue = true;
int taskDelayMs = 30000; // Task.DelayAsync for 20 seconds
//////////////////////////////////////////////////////
// Hold results from all calls. Outer Main has Id=-1
List<Event> Events = new List<Event>();
int taskCount = Environment.ProcessorCount << 1; // twice as many tasks as logical cores
if (induceIssue) taskCount = Environment.ProcessorCount << 5; // 2^5 as may tasks as cores
Console.WriteLine($"Starting {taskCount} tasks on {Environment.ProcessorCount} Logical Cores");
DateTime start = DateTime.Now;
DateTime end = start;
var te = new Event
{
Id = -1,
StartTime = start,
EndTime = end
};
// Add the event representing the entire "Main"
Events.Add(te);
using var cts = new CancellationTokenSource();
// record the time when we detected the token was triggered
cts.Token.Register(() => end = DateTime.Now);
// create an array of cancellable tasks
var tasks = (from i in Enumerable.Range(0, taskCount) select DoSomething(i, taskDelayMs, cts.Token)).ToArray();
// try with Task.Run to see if it makes a difference (it does now))
//var tasks = (from i in Enumerable.Range(0, taskCount) select Task.Run(async () => await DoSomething(i, taskDelayMs, cts.Token), cts.Token)).ToArray();
// CancelAfter is what we want, but let's call cancel, explicitly, to observe delays
//cts.CancelAfter(200);
// wait 100ms to trigger the cancellation so that we have a chance to enter into the Task.Delay(...) calls
DateTime cancelStart = DateTime.MinValue;
var triggerTask = Task.Run(async () =>
{
await Task.Delay(100);
cancelStart = DateTime.Now;
var triggerTime = (cancelStart - start).TotalSeconds;
Console.WriteLine($"Cancelling work after {triggerTime}s");
cts.Cancel();
DateTime cancelEnd = DateTime.Now;
triggerTime = (cancelEnd - start).TotalSeconds;
var cancelDuration = (cancelEnd - cancelStart).TotalSeconds;
// report time at which the token source finished the call to Cancel() (observe long delay)
Console.WriteLine($"After calling cancel: {triggerTime}s (ctr..Cancel() duration: {cancelDuration})");
te.CancelTriggerTime = DateTime.Now;
});
try
{
// use wait instead of when to pass the token into the WaitAll rather than relying on "DoSomething"
//Task.WaitAll(tasks, cts.Token);
Events.AddRange(await Task.WhenAll(tasks));
}
catch (OperationCanceledException oce)
{
// records the time when the exception threw the OperationCancelledException (if it is thrown)
te.CancelExceptionTime = DateTime.Now;
Console.WriteLine("Main Task Cancelled Exception");
}
te.EndTime = DateTime.Now;
var duration = (DateTime.Now - start).TotalSeconds;
var cancelAfter = (end - start).TotalSeconds;
await triggerTask;
//wait for them all to _actually_ finish
//Events.AddRange(await Task.WhenAll(tasks));
#region Build results String
var sb = new StringBuilder();
// sort the events by when the cancellation token was triggered
foreach (var e in Events.OrderBy(e => e.TriggeredAfter).ToList())
{
//sb.AppendLine(e.ToString());
sb.AppendLine(e.ReportCancel(cancelStart));
}
#endregion
// Write out all of the results
Console.Write(sb.ToString());
Console.WriteLine($"MainTask, taskDuration: {duration}, cancelAfter: {cancelAfter}");
Console.WriteLine("Done processing. Press any key");
Console.ReadKey();
}
static async Task<Event> DoSomething(int i, int delayMs, CancellationToken token)
{
Event e = new();
//lock (Events) Events.Add(e);
try
{
e.Id = i;
//lock(log) log.AppendLine($"{i} started");
e.StartTime = DateTime.Now;
e.EndTime = e.StartTime;
// record the time when we detected the token was triggered
token.Register(() => e.CancelTriggerTime = DateTime.Now);
if (token.IsCancellationRequested)
{
e.CancelledBeforeStart = true;
return e;
}
try
{
await Task.Delay(delayMs, token);
}
catch (TaskCanceledException tce)
{
e.CancelExceptionTime = DateTime.Now;
}
e.EndTime = DateTime.Now;
//await Task.Delay(20);
return e;
}
finally
{
e.EndTime = DateTime.Now;
}
}
}
}
Bah. Slow exception reporting in Visual Studio when run in debug mode. When I run it in release mode, it runs as expected.
My original problem involves more components than my example uses and the downstream code had issues that I already corrected.
Takeaway: If you are having performance issues, try it outside the debugger (Ctrl+F5 instead of F5, in VS). >.<
I have created a windows service application which is supposed to make a call to a restful api each one minute. I am actually running the API on my localhost device and putting a breakpoint to detect if the api being called, but nothing was received.
I have then copied exact same code to a console application and it seems being working as perfect. I am really new to windows services and I don't know if have any restrictions for calling an api within a service, so could you please tell me where is the issue
=================================================================
this function will be called each 1 minute
public async Task<DateTime> GetLastUpdatedDate(string branchName)
{
var VpsBaseUrl = System.Configuration.ConfigurationManager.AppSettings["VpsBaseUrl"];
HttpResponseMessage response = await client.GetAsync($"{VpsBaseUrl}/api/transactions/{branchName}");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<DateTime>(json);
Console.WriteLine(result.ToString());
return result;
}
else
{
return DateTime.Now.AddDays(-10);
}
}
==========================================================================
protected override void OnStart(string[] args)
{
//Create a new Timer with Interval set to seconds(1 Minutes).
System.Timers.Timer aTimer = new System.Timers.Timer(1 * 60 * 1000);
aTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimedEvent);
aTimer.AutoReset = true;
aTimer.Enabled = true;
aTimer.Start();
}
protected override void OnStop(){ }
//object source, System.Timers.ElapsedEventArgs e
private void OnTimedEvent(object source, System.Timers.ElapsedEventArgs e)
{
try
{
var task = Task.Run(async () =>
{
var x = await tProxy.GetLastUpdatedDate("branch1");
LogHelper.AppendToLogInfo(x.ToString());
});
}
catch(Exception ex)
{
LogHelper.AppendToLogError(ex.Message);
}
}
Let me clear you few points.
There is no different between calling API from Window Service or Window Form or Any Web API.
Context is important if account under which API execute does not have permission. (This could be second thing you can check).
One thing I will correct is
var task = Task.Run(async () =>
{
var x = await tProxy.GetLastUpdatedDate("branch1");
LogHelper.AppendToLogInfo(x.ToString());
});
task.Wait(); // It seems that you are missing this.
if you don't wait then it goes to next line and in your case it will get out of scope so possibly garbage collected.
It's possible that your configuration is not being read correctly in the Win32 service. Win32 services have their working directory set differently than console applications.
Try putting the following line of code at the top of your Main method:
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
I was wondering the best way to get round this issue.
I have created a Windows Service that connects to a mailbox, processes the emails, then cleans up after itself, waits a certain amount of time and repeats.
protected override void OnStart(string[] args)
{
this._mainTask = new Task(this.Poll, this._cancellationToken.Token, TaskCreationOptions.LongRunning);
this._mainTask.Start();
}
private void Poll()
{
CancellationToken cancellation = this._cancellationToken.Token;
TimeSpan interval = TimeSpan.Zero;
while (!cancellation.WaitHandle.WaitOne(interval))
{
using (IImapClient emailClient = new S22ImapClient())
{
ImapClientSettings chatSettings = ...;
emailClient.Connect(chatSettings); // CAN SOMETIMES HANG HERE
// SOME WORK DONE HERE
}
interval = this._waitAfterSuccessInterval;
// check the cancellation state.
if (cancellation.IsCancellationRequested)
{
break;
}
}
}
Now I am using a 3rd party IMAP client "S22.Imap". When I create the email client object on occasion it will hang on creation as it is attempting to login. This in turn will hang my Windows Service indefinitely.
public class S22ImapClient : IImapClient
{
private ImapClient _client;
public void Connect(ImapClientSettings imapClientSettings)
{
this._client = new ImapClient(
imapClientSettings.Host,
imapClientSettings.Port,
imapClientSettings.EmailAddress,
imapClientSettings.Password,
AuthMethod.Login,
true);
}
}
How would I change the "S22ImapClient.Connect()" call to, behind the covers, use some method to attempt to connect for a set amount of time, then abort if it has not been able to?
The solution to this will also be used for anything that I need to do with the mail client, for example "GetMessage()", "DeleteMessage()" etc
You could use a cancellation token source and give it a time to cancel after in the event that it hangs too long. Otherwise you would just have to extend the third party class and implement an async version of the Connect method. This is untested but should give you the basic idea.
private void Poll()
{
CancellationTokenSource source = new CancellationTokenSource();
TimeSpan interval = TimeSpan.Zero;
while (!source.Token.WaitHandle.WaitOne(interval))
{
using (IImapClient emailClient = new S22ImapClient())
{
ImapClientSettings chatSettings = ...;
var task = Task.Run(() =>
{
source.CancelAfter(TimeSpan.FromSeconds(5));
emailClient.Connect(chatSettings); // CAN SOMETIMES HANG HERE
}, source.Token);
// SOME WORK DONE HERE
}
interval = this._waitAfterSuccessInterval;
// check the cancellation state.
if (source.IsCancellationRequested)
{
break;
}
}
}
I decided to stop using the S22.Imap email client for this particular problem, and use another 3rd party component, ActiveUp MailSystem, as it includes async calls out the box.
This way I can do code like this:
IAsyncResult connectResult = this._client.BeginConnectSsl(imapClientSettings.Host, imapClientSettings.Port, null);
if (!connectResult.AsyncWaitHandle.WaitOne(this._connectionTimeout))
{
throw new EmailTimeoutException(this._connectionTimeout);
}
To clarify on my question I've been developing an app that does a lot of database updates / web service calls based on the input from a user (using an excel spreadsheet). If there are a lot of updates to make the process can take in excess of 20 minutes to run.
To stop my UI from freezing / timing out I've been looking into multithreading so I can run my long running process in an asynchronous manner and in the mean time simply displaying an animated gif whilst the process runs.
This all seems to run nicely at the moment with my test data, but when I substitute in the actual long running process I get an error regarding HttpContext.Current.User.Identity.Name. I've read up on this and from this article1 I took it to mean that if you set the 'Async' property to 'true' in the page directive and used the RegisterAsyncTask method you could then access HttpContext.Current. However, for me this doesn't seem to be true. I'm sure it's something I'm doing, so here is my code (I've mainly been using the following articles to write this article2 and article3):
ASP.NET page
<%# Page Title="Home Page" Async="true" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="false" CodeBehind="Index.aspx.cs" Inherits="MyApp.Index" %>
C# - RegisterAsyncTask is done on a button click, which starts the long running process:
protected void ProcessUpdates()
{
//Register async task to allow the processing of valid updates to occurr in the background
PageAsyncTask task = new PageAsyncTask(OnBegin, OnEnd, OnTimeOut, null);
RegisterAsyncTask(task);
}
IAsyncResult OnBegin(Object sender, EventArgs e, AsyncCallback cb, object state)
{
return Worker.BeginWork(cb, state);
}
private void OnEnd(IAsyncResult asyncResult)
{
//UpdateResults list should now have been filled and can be used to fill the datagrid
dgProcessedUpdates.DataSource = Worker.UpdateResults;
dgProcessedUpdates.CurrentPageIndex = 0;
dgProcessedUpdates.DataBind();
lblProgress.Text = "Update Results: update success / failure is shown below";
}
private void OnTimeOut(IAsyncResult asyncResult)
{
lblProgress.Text = "The process has timed out. Please check if any of the updates have been processed.";
}
C# - Worker class
public class Worker
{
public static List<AuditResult> UpdateResults = new List<AuditResult>();
private delegate void del();
//This method is called when the thread is started
public static IAsyncResult BeginWork(AsyncCallback cb, object state)
{
del processing = DoUpdateProcessing;
return processing.BeginInvoke(cb, state);
}
private static void DoUpdateProcessing()
{
//UpdateResults = ExcelFileProcessing.PassValidUpdates();
//Testing
Thread.Sleep(5000);
int i = 0;
while(i < 10)
{
AuditResult ar = new AuditResult();
ar.Result = "Successful";
ar.JobNumber = (1000 + i).ToString();
ar.NewValue = "Test New Value " + i.ToString();
ar.ResultDate = DateTime.Now.ToString();
ar.UserName = HttpContext.Current.User.Identity.Name;
UpdateResults.Add(ar);
i++;
}
}
}
Initially my test code didn't include a call to HttpContext.Current.User.Name for ar.UserName but after my issues with putting back in the call to ExcelFileProcessing.PassValidUpdates() with this I decided to do it. When I reach that part (ar.UserName = HttpContext.Current.User.Identity.Name) it says 'Object reference not set to an instance of an object', which suggests the HttpContext isn't carried across to the second thread. How can I do this?
UPDATE
I've currently reverted back to my previous code (that wasn't initially working) and simply passed the HttpContext.Current as a variable to my DoWork method as per this SO question like this:
Create 2nd thread
protected void ProcessValidUpdates()
{
Worker workerObject = new Worker();
HttpContext ctx = HttpContext.Current;
Thread workerThread = new Thread(new ThreadStart(() =>
{
HttpContext.Current = ctx;
workerObject.DoWork();
}));
workerThread.Start();
//Loop until worker thread activates
while (!workerThread.IsAlive) ;
//Put main thread to sleep to allow the worker thread to do some work
Thread.Sleep(1000);
//Request the worker thread stop itself
workerObject.RequestStop();
//Use the Join method to block the current thread until the object's thread terminates
workerThread.Join();
//UpdateResults list should now have been filled and can be used to fill the datagrid
dgProcessedUpdates.DataSource = Worker.UpdateResults;
dgProcessedUpdates.CurrentPageIndex = 0;
dgProcessedUpdates.DataBind();
lblProgress.Text = "Update Results: update success / failure is shown below";
}
Worker Class
public class Worker
{
//volatile hints to the compiler that this data member will be accessed by multiple threads.
private volatile bool _shouldStop;
public static List<AuditResult> UpdateResults = new List<AuditResult>();
//This method is called when the thread is started
public void DoWork()
{
while (!_shouldStop)
{
//Testing
Thread.Sleep(5000);
int i = 0;
while (i < 10)
{
AuditResult ar = new AuditResult();
ar.Result = "Successful";
ar.JobNumber = (1000 + i).ToString();
ar.NewValue = "Test New Value " + i.ToString();
ar.ResultDate = DateTime.Now.ToString();
ar.UserName = HttpContext.Current.User.Identity.Name;
UpdateResults.Add(ar);
i++;
}
}
}
public void RequestStop()
{
_shouldStop = true;
}
}
This seems to work in that I can now access HttpContext.Current and the username I expect. I think this is probably to some degree what some of you were proposing anyway. I appreciate the solution suggested by Andrew Morton but at the moment that would require a significant rewrite. At the moment my process already calls a web service to do the database stuff and returns a success or failure result. It also has to call another BPEL service directly. As such I suspect there may be further performance hits if I had to wrap all this into another web service. In addition, most calls to the process won't be that long running (probably less than 10 mins), so this is really only to address the few requests that exceed 20 mins. Finally, this is only likely to be used by 1 or 2 people, so it's unlikely to have a huge number of requests at 1 time.
However, bearing in mind my current solution, is there anything I should be aware of that might trip me up? IIS causing issues? Any additional help would be very much appreciated.
I have a site on a shared server. I need to have a BATCH job and I do that in another thread. It can run up to 1 hour (I ping the site so the worker process does not stop).
I went down the road of tying to get the current context. After many hours of research and searching it cannot be done. In a new thread the httpcontent.current is not there, it is not the same thread as the user was accessing, so the context did not carry over, and you cannot access the logged in user, since they are not logged into that thread.
I have 2 ASP.net 3.5 asmx web services, ws2 and ws3. They contain operations op21 and op31 respectively. op21 sleeps for 2 seconds and op31 sleeps for 3 seconds. I want to call both op21 and op31 from op11 in a web service, ws1, asynchronously. Such that when I call op11 from a client synchronously.,the time-taken will be 3 seconds which is the total. I currently get 5 seconds with this code:
WS2SoapClient ws2 = new WS2SoapClient();
WS3SoapClient ws3 = new WS3SoapClient();
//capture time
DateTime now = DateTime.Now;
//make calls
IAsyncResult result1 = ws3.BeginOP31(null,null);
IAsyncResult result2 = ws2.BeginOP21(null,null);
WaitHandle[] handles = { result1.AsyncWaitHandle, result2.AsyncWaitHandle };
WaitHandle.WaitAll(handles);
//calculate time difference
TimeSpan ts = DateTime.Now.Subtract(now);
return "Asynchronous Execution Time (h:m:s:ms): " + String.Format("{0}:{1}:{2}:{3}",
ts.Hours,
ts.Minutes,
ts.Seconds,
ts.Milliseconds);
The expected result is that the total time for both requests should be equal to the time it takes for the slower request to execute.
Note that this works as expected when I debug it with Visual Studio, however when running this on IIS, the time is 5 seconds which seems to show the requests are not processed concurrently.
My question is, is there a specific configuration with IIS and the ASMX web services that might need to be setup properly for this to work as expected?
Original Answer:
I tried this with google.com and bing.com am getting the same thing, linear execution. The problem is that you are starting the BeginOP() calls on the same thread, and the AsyncResult (for whatever reason) is not returned until the call is completed. Kind of useless.
My pre-TPL multi-threading is a bit rusty but I tested the code at the end of this answer and it executes asynchronously: This is a .net 3.5 console app. Note I obviously obstructed some of your code but made the classes look the same.
Update:
I started second-guessing myself because my execution times were so close to each other, it was confusing. So I re-wrote the test a little bit to include both your original code and my suggested code using Thread.Start(). Additionally, I added Thread.Sleep(N) in the WebRequest methods such that it should simulate vastly different execution times for the requests.
The test results do show that the code you posted was sequentially executed as I stated above in my original answer.
Note the total time is much longer in both cases than the actual web request time because of the Thread.Sleep(). I also added the Thread.Sleep() to offset the fact that the first web request to any site takes a long time to spin up (9 seconds), as can be seen above. Either way you slice it, it's clear that the times are sequential in the "old" case and truly "asynchronous" in the new case.
The updated program for testing this out:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
namespace MultiThreadedTest
{
class Program
{
static void Main(string[] args)
{
// Test both ways of executing IAsyncResult web calls
ExecuteUsingWaitHandles();
Console.WriteLine();
ExecuteUsingThreadStart();
Console.ReadKey();
}
private static void ExecuteUsingWaitHandles()
{
Console.WriteLine("Starting to execute using wait handles (old way) ");
WS2SoapClient ws2 = new WS2SoapClient();
WS3SoapClient ws3 = new WS3SoapClient();
IAsyncResult result1 = null;
IAsyncResult result2 = null;
// Time the threadas
var stopWatchBoth = System.Diagnostics.Stopwatch.StartNew();
result1 = ws3.BeginOP31();
result2 = ws2.BeginOP21();
WaitHandle[] handles = { result1.AsyncWaitHandle, result2.AsyncWaitHandle };
WaitHandle.WaitAll(handles);
stopWatchBoth.Stop();
// Display execution time of individual calls
Console.WriteLine((result1.AsyncState as StateObject));
Console.WriteLine((result2.AsyncState as StateObject));
// Display time for both calls together
Console.WriteLine("Asynchronous Execution Time for both is {0}", stopWatchBoth.Elapsed.TotalSeconds);
}
private static void ExecuteUsingThreadStart()
{
Console.WriteLine("Starting to execute using thread start (new way) ");
WS2SoapClient ws2 = new WS2SoapClient();
WS3SoapClient ws3 = new WS3SoapClient();
IAsyncResult result1 = null;
IAsyncResult result2 = null;
// Create threads to execute the methods asynchronously
Thread startOp3 = new Thread( () => result1 = ws3.BeginOP31() );
Thread startOp2 = new Thread( () => result2 = ws2.BeginOP21() );
// Time the threadas
var stopWatchBoth = System.Diagnostics.Stopwatch.StartNew();
// Start the threads
startOp2.Start();
startOp3.Start();
// Make this thread wait until both of those threads are complete
startOp2.Join();
startOp3.Join();
stopWatchBoth.Stop();
// Display execution time of individual calls
Console.WriteLine((result1.AsyncState as StateObject));
Console.WriteLine((result2.AsyncState as StateObject));
// Display time for both calls together
Console.WriteLine("Asynchronous Execution Time for both is {0}", stopWatchBoth.Elapsed.TotalSeconds);
}
}
// Class representing your WS2 client
internal class WS2SoapClient : TestWebRequestAsyncBase
{
public WS2SoapClient() : base("http://www.msn.com/") { }
public IAsyncResult BeginOP21()
{
Thread.Sleep(TimeSpan.FromSeconds(10D));
return BeginWebRequest();
}
}
// Class representing your WS3 client
internal class WS3SoapClient : TestWebRequestAsyncBase
{
public WS3SoapClient() : base("http://www.google.com/") { }
public IAsyncResult BeginOP31()
{
// Added sleep here to simulate a much longer request, which should make it obvious if the times are overlapping or sequential
Thread.Sleep(TimeSpan.FromSeconds(20D));
return BeginWebRequest();
}
}
// Base class that makes the web request
internal abstract class TestWebRequestAsyncBase
{
public StateObject AsyncStateObject;
protected string UriToCall;
public TestWebRequestAsyncBase(string uri)
{
AsyncStateObject = new StateObject()
{
UriToCall = uri
};
this.UriToCall = uri;
}
protected IAsyncResult BeginWebRequest()
{
WebRequest request =
WebRequest.Create(this.UriToCall);
AsyncCallback callBack = new AsyncCallback(onCompleted);
AsyncStateObject.WebRequest = request;
AsyncStateObject.Stopwatch = System.Diagnostics.Stopwatch.StartNew();
return request.BeginGetResponse(callBack, AsyncStateObject);
}
void onCompleted(IAsyncResult result)
{
this.AsyncStateObject = (StateObject)result.AsyncState;
this.AsyncStateObject.Stopwatch.Stop();
var webResponse = this.AsyncStateObject.WebRequest.EndGetResponse(result);
Console.WriteLine(webResponse.ContentType, webResponse.ResponseUri);
}
}
// Keep stopwatch on state object for illustration of individual execution time
internal class StateObject
{
public System.Diagnostics.Stopwatch Stopwatch { get; set; }
public WebRequest WebRequest { get; set; }
public string UriToCall;
public override string ToString()
{
return string.Format("Request to {0} executed in {1} seconds", this.UriToCall, Stopwatch.Elapsed.TotalSeconds);
}
}
}
There is some throttling in your system. Probably the service is configured for only one concurrent caller which is a common reason (WCF ConcurrencyMode). There might be HTTP-level connection limits (ServicePointManager.DefaultConnectionLimit) or WCF throttlings on the server.
Use Fiddler to determine if both requests are being sent simultaneously. Use the debugger to break on the server and see if both calls are running simultaneously.