Improving your C# Skills
上QQ阅读APP看书,第一时间看更新

Monitors

Monitors are used to provide thread-safe access to the resource. It is applicable to multithread programming, where there are multiple threads that need access to a resource simultaneously. When multiple threads attempt to enter monitor to access any resource, CLR allows only one thread at a time to enter and the other threads are blocked. When the thread exits the monitor, the next waiting thread enters, and so on.

If we look into the Monitor class, all the methods such as Monitor.Enter and Monitor.Exit operate on object references. Similarly to lock, Monitor also provides gated access to the resource; however, a developer will have greater control in terms of the API it provides.

Here is a basic example of using Monitor in .NET Core:

public class Job 
{ 
 
  int _jobDone; 
  object _lock = new object(); 
 
  public void IncrementJobCounter(int number) 
  { 
    Monitor.Enter(_lock); 
    // access to this field is synchronous
    _jobDone += number; 
    Monitor.Exit(_lock); 
  } 
 
} 

The preceding code snippet represents a job process where multiple threads are working on certain tasks. When the task completes, they call the IncrementJobCounter method to increment the _jobDone counter.

There are certain cases where the critical section has to wait for the resources to be available. Once they are available, we want to pulse the waiting block to execute.

To help us understand, let's take an example of a running Job whose task is to run the jobs added by multiple threads. If no job is present, it should wait for the threads to push and start executing them immediately.

In this example, we will create a JobExecutor class that runs in a separate thread. Here is the code snippet of JobExecutor

public class JobExecutor 
{ 
  const int _waitTimeInMillis = 10 * 60 * 1000; 
  private ArrayList _jobs = null; 
  private static JobExecutor _instance = null; 
  private static object _syncRoot = new object(); 
        
//Singleton implementation of JobExecutor public static JobExecutor Instance { get{ lock (_syncRoot) { if (_instance == null) _instance = new JobExecutor(); } return _instance; } } private JobExecutor() { IsIdle = true; IsAlive = true; _jobs = new ArrayList(); } private Boolean IsIdle { get; set; } public Boolean IsAlive { get; set; }
//Callers can use this method to add list of jobs public void AddJobItems(List<Job> jobList) {
//Added lock to provide synchronous access.
//Alternatively we can also use Monitor.Enter and Monitor.Exit
lock (_jobs) { foreach (Job job in jobList) { _jobs.Add(job); } //Release the waiting thread to start executing the //jobs
Monitor.PulseAll(_jobs); } }
/*Check for jobs count and if the count is 0, then wait for 10 minutes by calling Monitor.Wait. Meanwhile, if new jobs are added to the list, Monitor.PulseAll will be called that releases the waiting thread. Once the waiting is over it checks the count of jobs and if the jobs are there in the list, start executing. Otherwise, wait for the new jobs */ public void CheckandExecuteJobBatch() { lock (_jobs) { while (IsAlive) { if (_jobs == null || _jobs.Count <= 0) { IsIdle = true; Console.WriteLine("Now waiting for new jobs");
//Waiting for 10 minutes Monitor.Wait(_jobs, _waitTimeInMillis); } else { IsIdle = false; ExecuteJob(); } } } }
//Execute the job private void ExecuteJob() { for(int i=0;i< _jobs.Count;i++) { Job job = (Job)_jobs[i]; //Execute the job; job.DoSomething(); //Remove the Job from the Jobs list _jobs.Remove(job); i--; } } }

It's a singleton class, and other threads can access the JobExecutor instance using the static Instance property and call the AddJobsItems method to add the list of jobs to be executed. The CheckandExecuteJobBatch method runs continuously and checks for new jobs in the list every 10 minutes. Or, if it is interrupted by the AddJobsItems method by calling the Monitor.PulseAll method, it will immediately move to the while statement and check for the items count. If the items are present, the CheckandExecuteJobBatch method calls the ExecuteJob method that runs that job.

Here is the code snippet of the Job class containing two properties, namely JobID and JobName, and the DoSomething method that will print the JobID on the console:

public class Job 
{ 
  // Properties to set and get Job ID and Name
  public int JobID { get; set; } 
  public string JobName { get; set; } 
        
//Do some task based on Job ID as set through the JobID
//property public void DoSomething() { //Do some task based on Job ID Console.WriteLine("Executed job " + JobID); } }

Finally, on the main Program class, we can invoke three worker threads and one thread for JobExecutor, as shown in the following code:

class Program 
{ 
  static void Main(string[] args) 
  { 
    Thread jobThread = new Thread(new ThreadStart(ExecuteJobExecutor)); 
    jobThread.Start(); 
 
    //Starting three Threads add jobs time to time; 
    Thread thread1 = new Thread(new ThreadStart(ExecuteThread1)); 
    Thread thread2 = new Thread(new ThreadStart(ExecuteThread2)); 
    Thread thread3 = new Thread(new ThreadStart(ExecuteThread3)); 
    Thread1.Start(); 
    Thread2.Start(); 
    thread3.Start(); 
 
    Console.Read(); 
  } 
        
//Implementation of ExecuteThread 1 that is adding three
//jobs in the list and calling AddJobItems of a singleton
//JobExecutor instance
private static void ExecuteThread1() { Thread.Sleep(5000); List<Job> jobs = new List<Job>(); jobs.Add(new Job() { JobID = 11, JobName = "Thread 1 Job 1" }); jobs.Add(new Job() { JobID = 12, JobName = "Thread 1 Job 2" }); jobs.Add(new Job() { JobID = 13, JobName = "Thread 1 Job 3" }); JobExecutor.Instance.AddJobItems(jobs); }
//Implementation of ExecuteThread2 method that is also adding
//three jobs and calling AddJobItems method of singleton
//JobExecutor instance
private static void ExecuteThread2() { Thread.Sleep(5000); List<Job> jobs = new List<Job>(); jobs.Add(new Job() { JobID = 21, JobName = "Thread 2 Job 1" }); jobs.Add(new Job() { JobID = 22, JobName = "Thread 2 Job 2" }); jobs.Add(new Job() { JobID = 23, JobName = "Thread 2 Job 3" }); JobExecutor.Instance.AddJobItems(jobs); }
//Implementation of ExecuteThread3 method that is again
// adding 3 jobs instances into the list and
//calling AddJobItems to add those items into the list to execute
private static void ExecuteThread3() { Thread.Sleep(5000); List<Job> jobs = new List<Job>(); jobs.Add(new Job() { JobID = 31, JobName = "Thread 3 Job 1" }); jobs.Add(new Job() { JobID = 32, JobName = "Thread 3 Job 2" }); jobs.Add(new Job() { JobID = 33, JobName = "Thread 3 Job 3" }); JobExecutor.Instance.AddJobItems(jobs); } //Implementation of ExecuteJobExecutor that calls the
//CheckAndExecuteJobBatch to run the jobs public static void ExecuteJobExecutor() { JobExecutor.Instance.IsAlive = true; JobExecutor.Instance.CheckandExecuteJobBatch(); } }

The following is the output of running this code: