Let's go through it, step by step:
1.
class Program {
The job queue is a data structure:
private static Queue<Job> jobQueue;
If this data structure is accessed by multiple threads, you need to lock it:
private static void EnqueueJob(Job job) {
lock (jobQueue) {
jobQueue.Enqueue(job);
}
}
private static Job DequeueJob() {
lock (jobQueue) {
return jobQueue.Dequeue();
}
}
Let's add a method that retrieves a job from the web service and adds it to the queue:
private static void RetrieveJob(object unused) {
Job job = ... // retrieve job from webservice
EnqueueJob(job);
}
And a method that processes jobs in the queue in a loop:
private static void ProcessJobs() {
while (true) {
Job job = DequeueJob();
// process job
}
}
Let's run this program:
private static void Main() {
// run RetrieveJob every 5 minutes using a timer
Timer timer = new Timer(RetrieveJob);
timer.Change(TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(5));
// run ProcessJobs in thread
Thread thread = new Thread(ProcessJobs);
thread.Start();
// block main thread
Console.ReadLine();
}
}
2.
If you run the program, you'll notice that a job is added every 5 minutes. But jobQueue.Dequeue()
will throw an InvalidOperationException because the job queue is empty until a job is retrieved.
To fix that, we turn the job queue into a blocking queue by using a Semaphore:
private static Semaphore semaphore = new Semaphore(0, int.MaxValue);
private static void EnqueueJob(Job job) {
lock (jobQueue) {
jobQueue.Enqueue(job);
}
// signal availability of job
semaphore.Release(1);
}
private static Job DequeueJob() {
// wait until job is available
semaphore.WaitOne();
lock (jobQueue) {
return jobQueue.Dequeue();
}
}
3.
If you run the program again, it won't throw the exception and everything should work fine. But you'll notice that you have to kill the process because the ProcessJobs-thread never ends. So, how to you end your program?
I recommend you define a special job that indicates the end of job processing:
private static void ProcessJobs() {
while (true) {
Job job = DequeueJob();
if (job == null) {
break;
}
// process job
}
// when ProcessJobs returns, the thread ends
}
Then stop the timer and add the special job to the job queue:
private static void Main() {
// run RetrieveJob every 5 minutes using a timer
Timer timer = new Timer(RetrieveJob);
timer.Change(TimeSpan.FromMinutes(0), TimeSpan.FromMinutes(5));
// run ProcessJobs in thread
Thread thread = new Thread(ProcessJobs);
thread.Start();
// block main thread
Console.ReadLine();
// stop the timer
timer.Change(Timeout.Infinite, Timeout.Infinite);
// add 'null' job and wait until ProcessJobs has finished
EnqueueJob(null);
thread.Join();
}
I hope this implicitly answers all your questions :-)
Rules of thumb
Start a thread by specifying a method that has access to all necessary data structures
When accessing data structures from multiple threads, you need to lock the data structures
- In most cases the
lock
statement will do
- Use a ReaderWriterLockSlim if there are many threads reading from a data structure that is infrequently changed.
- You don't need a lock if the data structure is immutable.
When multiple threads depend on each other (e.g., a thread waiting for another thread to complete a task) use signals
Do not use Thread.Abort, Thread.Interrupt, Thread.Resume, Thread.Sleep, Thread.Suspend, Monitor.Pulse, Monitor.Wait