SoFunction
Updated on 2025-03-06

Synchronization within the c# process

In threads, if you need to share data, you must use synchronization technology to ensure that only one thread accesses and changes the state of shared data at a time. In .net, lock statements, Interlocked class, and Monitor class can be used for synchronization within the process.

1. Lock statement and thread safety

A lock statement is an easy way to set up and unlock. Before using the lock statement, enter another race condition. For example:

public class SharedState
{
  public int State { get; set; }
}
public class Job
{
  SharedState sharedState;
  public Job(SharedState sharedState)
  {
     = sharedState;
  }
  public void DoTheJob()
  {
    for (int i = 0; i < 50000; i++)
    {
         += 1;
    }
  }
}
static void Main()
{
  int numTasks = 20;
  var state = new SharedState();
  var tasks = new Task[numTasks];//Define 20 tasks
  for (int i = 0; i < numTasks; i++)
  {
    tasks[i] = (() => new Job(state).DoTheJob());//Start 20 tasks and modify the data at the same time  }

  for (int i = 0; i < numTasks; i++)
  {
    tasks[i].Wait();//Waiting for all tasks to end  }

  ("summarized {0}", );//Impossible output: summarized 1000000}

The actual output is not consistent with the expected output, and the output results are different for each run, but none of them are correct. If you reduce the number of threads, you will get the correct value more, but not every time.

Using the lock keyword, you can achieve synchronization problems when multiple threads access the same data. The lock statement indicates that a lock is waiting for the specified object to be locked, which can only refer to the type when it is referenced. After locking - only one thread is locked, the code in the lock statement block is run, and the lock is finally touched so that another thread can lock the object.

lock(obj)
{
  //Execute the code}
//Lock static members, so their type (object)lock(typeof(StaticCalss))
{
  //Execute the code}

So modify the above code and use SyncRoot mode. However, if access to attributes is locked:

public class SharedState
{
  private object syncRoot = new object();

  private int state = 0;
  public int State
  {
    get { lock (syncRoot) return state; }
    set { lock (syncRoot) state = value; }
  }
}

The previous contention situation will still occur. The method calls the get memory to get the current value of the state, and then set the new value to the state. During the call to the object's get and set memory, the object is not locked, and another thread can still get temporary values. The best way is to add the lock statement to the appropriate place in the call method without changing the SharedState class:

public class SharedState
{
  public int State { get; set; }
}
public class Job
{
  SharedState sharedState;
  public Job(SharedState sharedState)
  {
     = sharedState;
  }
  public void DoTheJob()
  {
    for (int i = 0; i < 50000; i++)
    {
      lock (sharedState)
      {
         += 1;
      }
    }
  }
}

Using lock statements in one place does not mean that other threads accessing the object are waiting. The synchronization function must be used for each thread display that accesses shared data.

To make the modification to state atomic operation, modify the code:

public class SharedState
{
  private int state = 0;
  public int State { get { return state; } }
  public int IncrementState()
  {
    lock (this)
    {
      return ++state;
    }
  }
}
//External accesspublic void DoTheJob()
{
  for (int i = 0; i &lt; 50000; i++)
  {
     ();    
  }
}

2. Interlocked class

The Interlocked class is used to atomize simple statements of variables. i++ is not thread-safe. It involves three steps: taking value, increasing self-increasing, and storing values. These operations may be interrupted by the thread scheduler. The Interlocked class provides methods to increment, decrement, exchange and read values ​​in a thread-safe way. The Interlocked class can only be used for simple synchronization problems, and it's very fast. Therefore, the code of the above IncrementState() method can be changed to: return (ref state);

3. Monitor class

The lcok statement will eventually be parsed by the C# compiler to use the Monitor class.

lock(obj)
{
  //Execute the code}

A simple lock(obj) statement will be parsed to call the Enter() method, which will wait until the thread locks the object. Only one thread can lock the object at a time. As long as the lock is unlocked, the thread can enter the synchronization stage. The Exit() method of the Monitor class unlocks. The compiler places the Exit() method in the finally of the try block. Regardless of whether the exception is thrown or not, it will unlock at the end of the statement block run.

(obj);
try
{
  //Execute the code}
finally
{
  (obj);
}

Compared to the lock statement, the Mpnitor class can set a timeout value waiting for locking. In this way, there will be no waiting for locking indefinitely. If the waiting locking time exceeds the specified time, false will be returned, indicating that it is not locked, the thread will no longer wait and perform other operations. Maybe later, the thread will try to obtain the lock again:

bool lockTaken = false;
(obj,500, ref lockTaken);//Whether the object is locked within 500msif (lockTaken)
{
  try
  {
    //Execute the code  }
  finally
  {
    (obj);
  }
}
else
{
  //No lock is obtained, code execution}

If the system overhead of an object-based locked object (Monitor) is too high due to garbage collection, you can use the SpinLock structure. , The SpinLock structure is suitable for: there are a lot of locking, and the locking time is always very short. Multiple SpinLock structures should be avoided, and do not call anything that may block.

The above is the detailed content of the synchronization within the c# process. For more information about the synchronization of the c# process, please pay attention to my other related articles!