Random message "The thread has exited with code 0" in managed threads - c#

I've a got a problem with the infamous message "The thread xxx has exited with code 0 (0x0)".
In my code I have a main class called "Load" that starts with a Windows Form load event:
public class Load
{
public Load()
{
Device[] devices = GetDevices(); // Get an array of devices from an external source
for (int i = 0; i < devices.Length; i++)
{
DeviceDiagnosticCtrl deviceDiagnostic = new DeviceDiagnosticCtrl(devices[i].name);
}
}
}
Inside the constructor, for each generic device read from an external source, I initialize a custom diagnostic class that runs a thread:
public class DeviceDiagnosticCtrl
{
private Thread diagnosticController;
private volatile bool diagnosticControllerIsRunning = false;
public DeviceDiagnosticCtrl(string _name)
{
// Thread initialization
this.diagnosticController = new Thread(new ThreadStart(this.CheckDiagnostic));
this.diagnosticController.Start();
this.diagnosticControllerIsRunning = true;
}
private void CheckDiagnostic()
{
while (this.diagnosticControllerIsRunning)
{
try
{
// Custom 'Poll' message class used to request diagnostic to specific device
Poll poll = new Poll();
// Generic Message result to diagnostic request
IGenericMessage genericResult;
// Use a custom driver to send diagnostic request
SendSyncMsgResult res = this.customDriver.SendSyncMessage(poll, out genericResult);
switch (res)
{
case SendSyncMessageResult.GOOD:
{
// Log result
}
break;
case SendSyncMessageResult.EXCEPTION:
{
// Log result
}
break;
}
Thread.Sleep(this.customDriver.PollScantime);
}
catch (Exception ex)
{
// Loggo exception
}
}
}
}
When I run the above code in debug mode I always read 8 devices from external source, and for each of them I continuously run a managed thread to retrieve diagnostic.
My problem is that randomly one or more of the 8 threads I expect from the code above exit with code 0, without any exception.
I've started/restarted the code in debug mode a lot of time, and almost everytime one of the thread exits.
I've read somewhere (i.e. this SO question) that it could depends of Garbage Collector action, but I'm not too sure if this is my case - and how to prevent it.
Do someone see something strange/wrong in the sample code I posted above? Any suggestion?

'while (this.diagnosticControllerIsRunning)' is quite likely to fail immediate, in which case the thread drops out. It's no good starting the thread and THEN setting 'this.diagnosticControllerIsRunning = true;' - you're quite likely to be too late.
Bolt/stable-door. Something like:
do{
lengthyStuff with Sleep() in it
}
while (this.diagnosticControllerRun);

Copied from Here
Right click in the Output window when you're running your program and
uncheck all of the messages you don't want to see (like Thread Exit
messages).

Related

stopped in the debugger with a C# thread hung on a lock statement, how do I figure out which thread currently owns the lock? [duplicate]

one of the threads in my application blocked at the following lock statement and resulted in a deadlock
void ExecuteCommand()
{
lock(this._lockinstance)
{
// do some operation
}
}
Is it possible to easily identify which thread is currently holding the lock?.. My application has more than 50 threads, which makes it difficult to go through each callstack using visual studio to locate the thread that holds the lock
Some sample code to try out:
class Test {
private object locker = new object();
public void Run() {
lock (locker) { // <== breakpoint here
Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
}
}
}
Set a breakpoint on the indicated line. When it breaks, use Debug + Windows + Memory + Memory 1. Right click the window and choose "4-byte Integer". In the Address box, type &locker. The 2nd word is the thread ID of the thread that owns the lock. Step past the lock statement to see it change.
Beware that the number is the managed thread ID, not the operating system thread ID that you see in the Debug + Windows + Threads window. That kinda sucks, you probably should add some logging to your program that dumps the value of ManagedThreadId so you have a way to match the value to a thread. Update: fixed in later VS versions, the Debug > Windows > Threads debugger window now shows the ManagedThreadId.
Recently I was trying to determine what function was holding a lock and found the following very useful and had not seen in demonstrated anywhere before. I've placed it as an answer here in case others find it useful too.
Many of the other solutions posted earlier require writing a new class and then converting of all lock(blah) to BetterLock(blah) which is a lot of work for debugging and which you may not want in the production/shipped version of your code. Others required having the debugger attached which changes the code's timing and could obscure the issue.
Instead, try the following...
Original code:
object obj = new object();
lock(obj)
{
// Do stuff
}
Modified code for debugging:
object _obj = new object();
object obj
{
get
{
System.Diagnostics.StackFrame frame = new System.Diagnostics.StackFrame(1);
System.Diagnostics.Trace.WriteLine(String.Format("Lock acquired by: {0} on thread {1}", frame.GetMethod().Name, System.Threading.Thread.CurrentThread.ManagedThreadId));
return _obj;
}
}
// Note that the code within lock(obj) and the lock itself remain unchanged.
lock(obj)
{
// Do stuff
}
By exposing obj as a property, at least temporarily, with very minimal code changes you can determine what function acquired the lock last and on what thread - just look at the Trace output for the last entry. Of course you can output any other information you might find useful in the getter as well.
No, this will not let you determine when a lock was released, but if it was getting released in a timely fashion, then you didn't actually have a lock contention issue in the first place.
You can implement a Monitor wrapper that saves stack traces & thread names on enter.
Old way:
private object myLock = new object();
...
lock(myLock)
{
DoSomething();
}
...
With code below:
private SmartLock myLock = new SmartLock();
...
myLock.Lock( () =>
{
DoSomething();
}
);
...
Source:
public class SmartLock
{
private object LockObject = new object();
private string HoldingTrace = "";
private static int WARN_TIMEOUT_MS = 5000; //5 secs
public void Lock(Action action)
{
try
{
Enter();
action.Invoke();
}
catch (Exception ex)
{
Globals.Error("SmartLock Lock action", ex);
}
finally
{
Exit();
}
}
private void Enter()
{
try
{
bool locked = false;
int timeoutMS = 0;
while (!locked)
{
//keep trying to get the lock, and warn if not accessible after timeout
locked = Monitor.TryEnter(LockObject, WARN_TIMEOUT_MS);
if (!locked)
{
timeoutMS += WARN_TIMEOUT_MS;
Globals.Warn("Lock held: " + (timeoutMS / 1000) + " secs by " + HoldingTrace + " requested by " + GetStackTrace());
}
}
//save a stack trace for the code that is holding the lock
HoldingTrace = GetStackTrace();
}
catch (Exception ex)
{
Globals.Error("SmartLock Enter", ex);
}
}
private string GetStackTrace()
{
StackTrace trace = new StackTrace();
string threadID = Thread.CurrentThread.Name ?? "";
return "[" + threadID + "]" + trace.ToString().Replace('\n', '|').Replace("\r", "");
}
private void Exit()
{
try
{
Monitor.Exit(LockObject);
HoldingTrace = "";
}
catch (Exception ex)
{
Globals.Error("SmartLock Exit", ex);
}
}
}
Yes, there is a 'Threads' view that you can use in VS. Break anywhere in your application (or click the 'Break All' button) then you can select each thread and view who has the lock (if anyone).
To add it, go to Debug > Windows > Threads (Ctrl+D,T)
Old posts are old.
But i thought i might give a solution i find to be fairly useful for trying to track down dead locks and other locking problems.
I use a disposable class for my lock - I like Monitor but any locking mechanism could be used.
public class MonitorLock : IDisposable
{
public static MonitorLock CreateLock(object value)
{
return new MonitorLock(value);
}
private readonly object _l;
protected MonitorLock(object l)
{
_l = l;
Console.WriteLine("Lock {0} attempt by {1}", _l, Thread.CurrentThread.ManagedThreadId);
Monitor.Enter(_l);
Console.WriteLine("Lock {0} held by {1}" , _l, Thread.CurrentThread.ManagedThreadId);
}
public void Dispose()
{
Monitor.Exit(_l);
Console.WriteLine("Lock {0} released by {1}", _l, Thread.CurrentThread.ManagedThreadId);
}
}
I use a lock object with a name so I can be clear as to which lock I'm trying to aquire.
public class LockObject
{
public string Name { get; set; }
public LockObject(string name)
{
Name = name;
}
public override string ToString()
{
return Name;
}
}
Finally create a lock object, and then in a using block hold the object.
//create an object to lock on
private readonly object _requestLock = new LockObject("_requestLock");
using (MonitorLock.CreateLock(_requestLock))
{
//do some work
}
Output should be something along the lines of
Lock _requestLock attempt by 92
Lock _requestLock held by 92
Lock _requestLock attempt by 19
Lock _requestLock released by 92
Lock _requestLock held by 19
Lock _requestLock released by 19
Hope that someone finds this useful :)
The Managed Stack Explorer from http://mse.codeplex.com/ or http://www.microsoft.com/downloadS/details.aspx?FamilyID=80cf81f7-d710-47e3-8b95-5a6555a230c2&displaylang=en is excellent in such cases.
It hooks into running managed code (appropriate permissions needed) including live code, and grabs a list of running threads. You can double-click on any of them or (more useful in cases like this) select the lot and hit enter for a quick relatively non-invasive (obviously it's going to consume resources, but it goes in and out as quickly as it can) dump of the current stacks of different threads. Great for finding a deadlock, infinite loop, near-infinite loop (for those times when your application accidentally depends upon astronomers being pessimistic about how long the earth will last to have a hope of completing) and other such cases.
I'm not sure in which version this feature was added, but the Visual Studio 2022 debugger now shows in its Call Stack window the ID of the thread that owns the lock on which another thread is waiting to acquire, e.g.,
I found this over here.

C# BackgroundWorker Exiting Unexpectedly

I have thef ollowing background worker in my app which is meant to start a user's session automatically if there is not already one available.
This is done on a backgroundworker (backgroundInit) on initialisation. As you can see below, I have a while loop which continues to run as long as the var checker remains false:
var checker = false;
var i = 0;
while (checker == false)
{
_session = funcs.GetSession(_servers, _name);
_sessID = _session[0].Trim();
_servName = _session[1];
checker = funcs.CheckRunning("lync.exe");
i++;
if (i > 200)
{
break;
}
}
The CheckRunning method just checks if a specified program (in this case, "lync") is currently running and returns either true or false accordingly (This is done via a CMD command).
When I run the app in an empty session however, the while loop only iterates one time before breaking out, even though "Lync" is definitely not running.
Is there any reason why running a process or too many processes from within a Backgroundworker may cause it to exit?
As the comments mentioned, this was not an issue with the BackgroundWorker, but rather an exception occurring at _sessID = session[0].Trim(); where the session had not yet started, so there is no ID.
To resolve this, I simply placed a Try/Catch block around this assignment, and let the program silently ignore the exception:
try
{
_sessID = _session[0].Trim();
_servName = _session[1];
}
catch (Exception exp)
{
// MessageBox.Show(exp.Message);
}
This works for me, as the loop will continue checking until the counter i reaches the 200 limit, at which stage the program will accept failure.

SQL CLR make sure finally block is executed

I have a SQL server CLR stored proc that is used to retrieve a large set of rows, then do a process and update a count in another table.
Here's the flow:
select -> process -> update count -> mark the selected rows as processed
The nature of the process is that it should not count the same set of data twice. And the SP is called with a GUID as an argument.
So I'm keeping a list of GUIDs (in a static list in the SP) that are currently in process and halt the execution for subsequent calls to the SP with the same argument until one currently in process finishes.
I have the code to remove the GUID when a process finishes in a finally block but it's not working everytime. There are instances (like when the user cancels the execution of the SP)where the SP exits without calling the finally block and without removing the GUID from the list so subsequent calls keeps waiting indefinitely.
Can you guys give me a solution to make sure that my finally block will be called no matter what or any other solution to make sure only one ID is in process at any given time.
Here's a sample of the code with the processing bits removed
[Microsoft.SqlServer.Server.SqlProcedure]
public static void TransformSurvey(Guid PublicationId)
{
AutoResetEvent autoEvent = null;
bool existing = false;
//check if the process is already running for the given Id
//concurrency handler holds a dictionary of publicationIds and AutoresetEvents
lock (ConcurrencyHandler.PublicationIds)
{
existing = ConcurrencyHandler.PublicationIds.TryGetValue(PublicationId, out autoEvent);
if (!existing)
{
//there's no process in progress. so OK to start
autoEvent = new AutoResetEvent(false);
ConcurrencyHandler.PublicationIds.Add(PublicationId, autoEvent);
}
}
if (existing)
{
//wait on the shared object
autoEvent.WaitOne();
lock (ConcurrencyHandler.PublicationIds)
{
ConcurrencyHandler.PublicationIds.Add(PublicationId, autoEvent); //add this again as the exiting thread has removed this from the list
}
}
try
{
// ... do the processing here..........
}
catch (Exception ex)
{
//exception handling
}
finally
{
//remove the pubid
lock (ConcurrencyHandler.PublicationIds)
{
ConcurrencyHandler.PublicationIds.Remove(PublicationId);
autoEvent.Set();
}
}
}
Wrapping the code at a higher level is a good solution, another option could be the using statement with IDisposable.
public class SQLCLRProcedure : IDisposable
{
public bool Execute(Guid guid)
{
// Do work
}
public void Dispose()
{
// Remove GUID
// Close Connection
}
}
using (SQLCLRProcedure procedure = new SQLCLRProcedure())
{
procedure.Execute(guid);
}
This isn't verified in a compiler but it's commonly referred to as the IDisposable Pattern.
http://msdn.microsoft.com/en-us/library/system.idisposable.aspx

Serially process ConcurrentQueue and limit to one message processor. Correct pattern?

I'm building a multithreaded app in .net.
I have a thread that listens to a connection (abstract, serial, tcp...).
When it receives a new message, it adds it to via AddMessage. Which then call startSpool. startSpool checks to see if the spool is already running and if it is, returns, otherwise, starts it in a new thread. The reason for this is, the messages HAVE to be processed serially, FIFO.
So, my questions are...
Am I going about this the right way?
Are there better, faster, cheaper patterns out there?
My apologies if there is a typo in my code, I was having problems copying and pasting.
ConcurrentQueue<IMyMessage > messages = new ConcurrentQueue<IMyMessage>();
const int maxSpoolInstances = 1;
object lcurrentSpoolInstances;
int currentSpoolInstances = 0;
Thread spoolThread;
public void AddMessage(IMyMessage message)
{
this.messages.Add(message);
this.startSpool();
}
private void startSpool()
{
bool run = false;
lock (lcurrentSpoolInstances)
{
if (currentSpoolInstances <= maxSpoolInstances)
{
this.currentSpoolInstances++;
run = true;
}
else
{
return;
}
}
if (run)
{
this.spoolThread = new Thread(new ThreadStart(spool));
this.spoolThread.Start();
}
}
private void spool()
{
Message.ITimingMessage message;
while (this.messages.Count > 0)
{
// TODO: Is this below line necessary or does the TryDequeue cover this?
message = null;
this.messages.TryDequeue(out message);
if (message != null)
{
// My long running thing that does something with this message.
}
}
lock (lcurrentSpoolInstances)
{
this.currentSpoolInstances--;
}
}
This would be easier using BlockingCollection<T> instead of ConcurrentQueue<T>.
Something like this should work:
class MessageProcessor : IDisposable
{
BlockingCollection<IMyMessage> messages = new BlockingCollection<IMyMessage>();
public MessageProcessor()
{
// Move this to constructor to prevent race condition in existing code (you could start multiple threads...
Task.Factory.StartNew(this.spool, TaskCreationOptions.LongRunning);
}
public void AddMessage(IMyMessage message)
{
this.messages.Add(message);
}
private void Spool()
{
foreach(IMyMessage message in this.messages.GetConsumingEnumerable())
{
// long running thing that does something with this message.
}
}
public void FinishProcessing()
{
// This will tell the spooling you're done adding, so it shuts down
this.messages.CompleteAdding();
}
void IDisposable.Dispose()
{
this.FinishProcessing();
}
}
Edit: If you wanted to support multiple consumers, you could handle that via a separate constructor. I'd refactor this to:
public MessageProcessor(int numberOfConsumers = 1)
{
for (int i=0;i<numberOfConsumers;++i)
StartConsumer();
}
private void StartConsumer()
{
// Move this to constructor to prevent race condition in existing code (you could start multiple threads...
Task.Factory.StartNew(this.spool, TaskCreationOptions.LongRunning);
}
This would allow you to start any number of consumers. Note that this breaks the rule of having it be strictly FIFO - the processing will potentially process "numberOfConsumer" elements in blocks with this change.
Multiple producers are already supported. The above is thread safe, so any number of threads can call Add(message) in parallel, with no changes.
I think that Reed's answer is the best way to go, but for the sake of academics, here is an example using the concurrent queue -- you had some races in the code that you posted (depending upon how you handle incrementing currnetSpoolInstances)
The changes I made (below) were:
Switched to a Task instead of a Thread (uses thread pool instead of incurring the cost of creating a new thread)
added the code to increment/decrement your spool instance count
changed the "if currentSpoolInstances <= max ... to just < to avoid having one too many workers (probably just a typo)
changed the way that empty queues were handled to avoid a race: I think you had a race, where your while loop could have tested false, (you thread begins to exit), but at that moment, a new item is added (so your spool thread is exiting, but your spool count > 0, so your queue stalls).
private ConcurrentQueue<IMyMessage> messages = new ConcurrentQueue<IMyMessage>();
const int maxSpoolInstances = 1;
object lcurrentSpoolInstances = new object();
int currentSpoolInstances = 0;
public void AddMessage(IMyMessage message)
{
this.messages.Enqueue(message);
this.startSpool();
}
private void startSpool()
{
lock (lcurrentSpoolInstances)
{
if (currentSpoolInstances < maxSpoolInstances)
{
this.currentSpoolInstances++;
Task.Factory.StartNew(spool, TaskCreationOptions.LongRunning);
}
}
}
private void spool()
{
IMyMessage message;
while (true)
{
// you do not need to null message because it is an "out" parameter, had it been a "ref" parameter, you would want to null it.
if(this.messages.TryDequeue(out message))
{
// My long running thing that does something with this message.
}
else
{
lock (lcurrentSpoolInstances)
{
if (this.messages.IsEmpty)
{
this.currentSpoolInstances--;
return;
}
}
}
}
}
Check 'Pipelines pattern': http://msdn.microsoft.com/en-us/library/ff963548.aspx
Use BlockingCollection for the 'buffers'.
Each Processor (e.g. ReadStrings, CorrectCase, ..), should run in a Task.
HTH..

How to prevent doubled timer in Global.asax?

Description:
On a C# ASP.Net web application, we have implemented some timers to periodically run background tasks. One of the timers occasionally seems to get "doubled" or more rarely "tripled".
The timer is set to run once every minute and seems to run properly for a while. Eventually, however, it seems like a second timer gets started and calls the timed process a second time within the same time interval. I've even seen a case where we had three processes running.
Since this process locks some database records and having a second (or third) process doing the same thing will cause a deadlock or timeout error on the database connection, we've implemented a mechanism to only allow one thread at a time to execute the database critical portion of the process code. When the process takes longer than a minute to run, this mechanism successfully blocks the next run triggered by its own timer. But the thread locking fails if the process is triggered by the second (or third) timer.
In our logs, I output both the Process ID and the Managed Thread ID, which lets me see which thread is starting, finishing, or erring out. The strange thing, is that regardless of which timer instance kicked off the process, the Process ID is the same.
var processID = System.Diagnostics.Process.GetCurrentProcess().Id;
var thread = System.Threading.Thread.CurrentThread.ManagedThreadId;
How do I prevent multiple instances of the timer?
We have a web-farm with 2 servers behind a load balancer. I've been assurred that the web-garden is set to only allow one instance of the app-pool on each server. A web.config setting specifies which server will run the timed process. The other server will not load the timer.
Relevant Code:
On the Global.asax.cs
protected static WebTaskScheduler PersonGroupUpdateScheduler
{
get;
private set;
}
protected void StartSchedulers()
{
using (var logger = new LogManager())
{
// ... other timers configured in similar fashion ...
if (AppSetting.ContinuousPersonGroupUpdates)
{
// clear out-of-date person-group-updater lock
logger.AppData.Remove("PersonGroupUpdater"); // database record to prevent interference with another process outside the web application.
var currentServer = System.Windows.Forms.SystemInformation.ComputerName;
if (currentServer.EqualsIngoreCase(AppSetting.ContinuousPersonGroupUpdateServer))
{
PersonGroupUpdateScheduler = new WebTaskScheduler() {
AutoReset = true,
Enabled = true,
Interval = AppSetting.ContinuousPersonGroupUpdateInterval.TotalMilliseconds,
SynchronizingObject = null,
};
PersonGroupUpdateScheduler.Elapsed += new ElapsedEventHandler(DistributePersonGroupProcessing);
PersonGroupUpdateScheduler.Start();
HostingEnvironment.RegisterObject(PersonGroupUpdateScheduler);
logger.Save(Log.Types.Info, "Starting Continuous Person-Group Updating Timer.", "Web");
}
else
{
logger.Save(Log.Types.Info, string.Format("Person-Group Updating set to run on server {0}.", AppSetting.ContinuousPersonGroupUpdateServer), "Web");
}
}
else
{
logger.Save(Log.Types.Info, "Person-Group Updating is turned off.", "Web");
}
}
}
private void DistributePersonGroupProcessing(object state, ElapsedEventArgs eventArgs)
{
// to start with a clean connection, create a new data context (part of default constructor)
// with each call.
using (var groupUpdater = new GroupManager())
{
groupUpdater.HttpContext = HttpContext.Current;
groupUpdater.ContinuousGroupUpdate(state, eventArgs);
}
}
On a separate file, we have the WebTaskScheduler class which just wraps System.Timers.Timer and implements the IRegisteredObject interface so that IIS will recognize the triggered process as something it needs to deal with when shutting down.
public class WebTaskScheduler : Timer, IRegisteredObject
{
private Action _action = null;
public Action Action
{
get
{
return _action;
}
set
{
_action = value;
}
}
private readonly WebTaskHost _webTaskHost = new WebTaskHost();
public WebTaskScheduler()
{
}
public void Stop(bool immediate)
{
this.Stop();
_action = null;
}
}
Finally, the locking mechanism for the critical section of the code.
public void ContinuousGroupUpdate(object state, System.Timers.ElapsedEventArgs eventArgs)
{
var pgUpdateLock = PersonGroupUpdaterLock.Instance;
try
{
if (0 == Interlocked.Exchange(ref pgUpdateLock.LockCounter, 1))
{
if (LogManager.AppData["GroupImporter"] == "Running")
{
Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
LogManager.Save(Log.Types.Info, string.Format("Group Import is running, exiting Person-Group Updater. Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
return;
}
try
{
LogManager.Save(Log.Types.Info, string.Format("Continuous Person-Group Update is Starting. Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
LogManager.AppData["PersonGroupUpdater"] = "Running";
// ... prep work is done here ...
try
{
// ... real work is done here ...
LogManager.Save(Log.Types.Info, "Continuous Person-Group Update is Complete", "Person-Group Updater");
}
catch (Exception ex)
{
ex.Data["Continuous Person-Group Update Activity"] = "Processing Groups";
ex.Data["Current Record when failure occurred"] = currentGroup ?? string.Empty;
LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
}
}
catch (Exception ex)
{
LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
}
finally
{
Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
LogManager.AppData.Remove("PersonGroupUpdater");
}
}
else
{
// exit if another thread is already running this method
LogManager.Save(Log.Types.Info, string.Format("Continuous Person-Group Update is already running, exiting Person-Group Updater. Person-Group Update Signaled at {0:HH:mm:ss.fff}.", eventArgs.SignalTime), "Person-Group Updater");
}
}
catch (Exception ex)
{
Interlocked.Exchange(ref pgUpdateLock.LockCounter, 0);
LogManager.Save(Log.Types.Error, ex, "Person-Group Updater");
}
}
IIS can/will host multiple AppDomains under a worker process (w3wp). These AppDomains can't/don't/shouldn't really talk to each. It's IIS's responsibility to manage them.
I suspect what's happening is that you have multiple AppDomains loaded.
That said...just to be 100% sure...the timer is being started under Application_Start in your global.asax, correct? This will get executed once per AppDomain (not per HttpApplication, as it's name suggests).
You can check how many app domains are running for your process by using the ApplicationManager's GetRunningApplications() and get GetAppDomain(string id) methods.
In theory you could also do some inter-appdomain communication in there to make sure your process only starts once...but I'd strongly advise against it. In general, relying on scheduling from a web application is ill advised (because your code is meant to be ignorant of how IIS manages your application lifetime).
The preferred/recommended approach for scheduling is via a Windows Service.

Categories

Resources