[C#] Various locks

overview

Lock: Solve the data sharing security problem in multithreading.

As soon as thread synchronization is mentioned, locks are mentioned. As one of the means of thread synchronization, locks are always questioned. On the one hand, the use of locks is very simple. As long as you add locks where the code does not want to be re-entered (where multiple threads are executed at the same time), it can be guaranteed that at most one thread of the code is executing at any time; , the lock is not as simple as it seems, the lock will cause many problems: performance degradation, deadlock, etc. Using the volatile keyword or the methods provided in Interlocked can avoid the use of locks, but the functions of these atomic operations are limited, and many operations are troublesome to implement, such as unordered thread-safe collections.

Lock

user mode lock

1. The volatile keyword

Volatile does not achieve real thread synchronization, and the operation level stays at the variable level rather than the atomic level. For a single system processor, variables are stored in the main memory and have no chance to be modified by others. However, if it is multi-processor, there may be problems, because each processor has a separate data cache, and data updates may not be written back to the main memory immediately, which may cause asynchrony.

2. Spinlock rotation lock

Spinlock is a relatively common lock mechanism provided in the kernel. Spinlock is a "wait in place" method to solve resource conflicts, that is, after one thread acquires a spinlock, another thread expects to acquire the spinlock The lock cannot be obtained, and it can only "spin" in place (busy waiting). Due to the busy waiting feature of the spin lock, it is destined to be limited in its usage scenarios: the spin lock should not be held for a long time (consuming CPU resources).

SpinLock spinLock = new SpinLock();
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
spinLock.Exit();

kernel mode lock

Divided into: event locks, semaphores, mutexes, read-write locks.

Suggestion: It is generally not recommended to use kernel-mode locks casually, and the resource expenditure is relatively large. We can use hybrid locks instead, and the lock keyword we'll talk about shortly.

1. Event lock

Automatic event lock: AutoResetEvent

WaitOne() enters the wait, and Set() will release the current lock to a waiting thread.

var are = new AutoResetEvent(true);
are.WaitOne();
//...
are.Set();

Manual event lock: ManualResetEvent

WaitOne() enters the wait, and Set() will release the current lock to all waiting threads.

var mre = new ManualResetEvent(false);
 
mre.WaitOne();//批量拦截,后续的省略号部分是无序执行的。
//...
mre.Set();//一次释放给所有等待线程

2, signal amount

Semaphore: Semaphore

The semaphore can control the number of threads passing through at the same time and the total number of threads.

//第一个参数表示同时可以允许的线程数,比如1表示每次只允许一个线程通过,
//第二个是最大值,比如8表示最多有8个线程。
var semaphore = new Semaphore(1, 8);

3. Mutex lock

Mutex: Mutex

Mutex is very close to Monitor, but it does not have the wake-up function of Monitor.Pulse, Wait, and PulseAll. Its advantage is that it can cross processes, and can share a mutex between different processes on the same machine or even remote robots.

var mutex = new Mutex();
mutex.WaitOne();
//...
mutex.ReleaseMutex();

4. Read-write lock

Read-write lock: ReaderWriterLock

Don't use ReaderWriterLock, this class has problems (deadlock, performance), use ReaderWriterLockSlim

.NET Framework has two readers - Writer Lock, ReaderWriterLockSlim and ReaderWriterLock. It is recommended to use ReaderWriterLockSlim for all newly developed projects. While ReaderWriterLockSlim is similar to ReaderWriterLock, it differs in that it simplifies the recursion rules and the lock state promotion and downgrade rules. ReaderWriterLockSlim avoids many potential deadlock situations. Also, ReaderWriterLockSlim performs significantly better than ReaderWriterLock.

Note: Read-write locks do not start from the perspective of limiting the number of threads. Instead, it is divided according to the function of reading and writing.

The basic scheme of read-write lock: multiple threads can read together, and only one thread can write.
  
  Read-write lock: ReaderWriterLockSlim

//源码摘录自微软官网
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
 
public class SynchronizedCache 
{
    
    
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();
 
    public int Count
    {
    
     get {
    
     return innerCache.Count; } }
 
    public string Read(int key)
    {
    
    
        cacheLock.EnterReadLock();
        try
        {
    
    
            return innerCache[key];
        }
        finally
        {
    
    
            cacheLock.ExitReadLock();
        }
    }
 
    public void Add(int key, string value)
    {
    
    
        cacheLock.EnterWriteLock();
        try
        {
    
    
            innerCache.Add(key, value);
        }
        finally
        {
    
    
            cacheLock.ExitWriteLock();
        }
    }
 
    public bool AddWithTimeout(int key, string value, int timeout)
    {
    
    
        if (cacheLock.TryEnterWriteLock(timeout))
        {
    
    
            try
            {
    
    
                innerCache.Add(key, value);
            }
            finally
            {
    
    
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
    
    
            return false;
        }
    }
 
    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
    
    
        cacheLock.EnterUpgradeableReadLock();
        try
        {
    
    
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
    
    
                if (result == value)
                {
    
    
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
    
    
                    cacheLock.EnterWriteLock();
                    try
                    {
    
    
                        innerCache[key] = value;
                    }
                    finally
                    {
    
    
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
    
    
                cacheLock.EnterWriteLock();
                try
                {
    
    
                    innerCache.Add(key, value);
                }
                finally
                {
    
    
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
    
    
            cacheLock.ExitUpgradeableReadLock();
        }
    }
 
    public void Delete(int key)
    {
    
    
        cacheLock.EnterWriteLock();
        try
        {
    
    
            innerCache.Remove(key);
        }
        finally
        {
    
    
            cacheLock.ExitWriteLock();
        }
    }
 
    public enum AddOrUpdateStatus
    {
    
    
        Added,
        Updated,
        Unchanged
    };
 
    ~SynchronizedCache()
    {
    
    
       if (cacheLock != null) cacheLock.Dispose();
    }
}
 
ReaderWriterLockSlim示例

dynamic count

Dynamic count lock CountdownEvent

  • CountdownEvent: A mechanism to limit the number of threads, and this is also commonly used (it is also a kind of semaphore).
  • Usage scenario: read data from a certain table based on multiple threads: for example, we have A, B, C... Each data table we want to read through multiple threads. Because if one thread is used, the data volume will be stuck.

Example:

Table A: 10w data – "10 threads read, 1 thread 1w data.
Table B: 5w data--"5 threads and 1 thread 1w
Table C: 1w data--"2 threads and 1 thread 5k

private static CountdownEvent countdownEvent = new CountdownEvent(10);
//默认10个threadcount初始值,一个线程用一个就减掉1,直到为0后,相当于结束
static void LoadData()
{
    
    
    countdownEvent.Reset(10);//重置当前ThreadCount上限
    for (int i = 0; i < 10; i++)
    {
    
    
        Task.Factory.StartNew(() =>
                              {
    
    
                                  Thread.Sleep(500);
                                  LoadTableA();
                              });
    }

    //阻止当前线程,直到设置了System.Threading.CountdonwEvent为止
    countdownEvent.Wait();//相当于Task.WaitAll()

    Console.WriteLine("TableA加载完毕..........\r\n");

    //加载B表
    countdownEvent.Reset(5);
    for (int i = 0; i < 5; i++)
    {
    
    
        Task.Factory.StartNew(() =>
                              {
    
    
                                  Thread.Sleep(500);
                                  LoadTableB();
                              });
    }
    countdownEvent.Wait();
    Console.WriteLine("TableB加载完毕..........\r\n");

    //加载C表
    myLock7.Reset(2);
    for (int i = 0; i < 2; i++)
    {
    
    
        Task.Factory.StartNew(() =>
                              {
    
    
                                  Thread.Sleep(500);
                                  LoadTableC();
                              });
    }
    countdownEvent.Wait();
    Console.WriteLine("TableC加载完毕..........\r\n");
}

/// <summary>
/// 加载A表
/// </summary>
private static void LoadTableA()
{
    
    
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableA正在加载中...{
      
      Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();//将当前的ThreadCount--   操作,就是减掉一个值
}

/// <summary>
/// 加载B表
/// </summary>
private static void LoadTableB()
{
    
    
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableB正在加载中...{
      
       Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();
}

/// <summary>
/// 加载C表
/// </summary>
private static void LoadTableC()
{
    
    
    //在这里编写具体的业务逻辑...
    Console.WriteLine($"当前TableC正在加载中...{
      
      Thread.CurrentThread.ManagedThreadId}");
    countdownEvent.Signal();
}

Atomic operation class: Interlocked

The Interlocked class provides four methods for atomic-level variable operations. Increment, Decrement, Exchange, and CompareExchange.

a. Using Increment and Decrement can guarantee that the addition and subtraction of an integer is an atomic operation.

b. The Exchange method automatically exchanges the value of the specified variable.

c. The CompareExchange method combines two operations: comparing two values ​​and storing a third value in one of the variables based on the result of the comparison.

d. Comparison and exchange operations are also performed as atomic operations. Interlocked.CompareExchange(ref a, b, c); Atomic operation, compare a parameter with c parameter, equal b replaces a, unequal does not replace.

monitor lock

Monitor A lock that limits the number of threads.

lock keyword

The essence is the syntactic sugar of Monitor

  • Use ILSyp to observe the generated code and find that it is the same as Monitor
  • The internal mechanism of lock/Monitor is essentially to use the upper "synchronization block" to realize resource locking.
  • PS: Among the many locks above, only Monitor has syntactic sugar, which shows that this is important.

Monitor

Locked resources must be accessible to accessible threads, so they cannot be local variables.
Locked resources must never be value types.
lock cannot lock the string type, although it is a reference type (this may be questionable).

private static object syncRoot = new object();
private int num;

//【1】简单写法
static void TestMethod1()
{
    
    
    for (int i = 0; i < 100; i++)
    {
    
    
        Monitor.Enter(syncRoot);//锁住资源
        num++;
        Console.WriteLine(num);
        Monitor.Exit(syncRoot);//退出资源
    }
}

//【2】严谨的写法(更常用的写法)
static void TestMethod2()
{
    
    
    for (int i = 0; i < 100; i++)
    {
    
    
        bool taken = false;
        try
        {
    
    
            Monitor.Enter(syncRoot, ref taken);//这个类似于SpinLock
            num++;
            Console.WriteLine(num);
        }
        catch (Exception ex)
        {
    
    
            Console.WriteLine(ex.Message);
        }
        finally
        {
    
    
            if (taken)
            {
    
    
                Monitor.Exit(syncRoot);
            }
        }
    }
}

//总结:为了严谨性,保证程序正常秩序,我们在锁区域添加了异常处理,还要添加判断,非常麻烦。我们可以使用语法糖Lock。
//语法糖:只是编译器层面的,底层代码生成还是跟以前一样的。
static void Method11()
{
    
    
    for (int i = 0; i < 100; i++)
    {
    
    
        lock (syncRoot)
        {
    
    
            num++;
            Console.WriteLine(num);
        }
    }
}

source

Several locks in C#: user mode lock, kernel mode lock, dynamic count, monitoring
C# lock summary

Guess you like

Origin blog.csdn.net/weixin_44231544/article/details/131657030