[C#] Parallel programming practice: synchronization primitives (2)

         Potential problems of parallel programming are discussed in Chapter 4, one of which is synchronization overhead. When work is broken down into work items and processed by tasks, there is a need to synchronize the results of each thread. Thread-local storage and partition-local storage can solve the synchronization problem to some extent. However, when data is shared, synchronization primitives are required.

        Due to space limitation, this chapter is the second part. Focuses on locks, mutexes, and semaphores.

        This tutorial corresponds to Learning Engineering: Magician Dix / HandsOnParallelProgramming · GitCode


5. Locks, mutexes and semaphores

        Lock (Lock) and Mutex (Mutex) are lock structures that allow only one thread to access protected resources. Semaphore is also a lock structure that allows a specified number of threads to access protected resources.

synchronization primitives

number of threads allocated

cross process

Lock

1

×

Mutex

1

Semaphore

many

Lightweight semaphore (SemaphoreSlim)

many

×

        Because locks restrict other threads from accessing shared resources, it is important not to lock code that blocks, or performance will drop dramatically. Generally speaking, locks are only used for critical sections .

Critical Section:

        Part of a thread's execution path that must be protected against concurrent access, thereby maintaining some invariant. A critical section is not itself a synchronization primitive, but it depends on a synchronization primitive.

5.1, lock

        In the previous code, let's make a transformation:

        private static object lockObj = new object();
        public static void RunTestAddFunctionWithLock()
        {
            TestValueA = 0;
            TestValueB = 0;
            m_IsFinishOnce = false;

            Task.Run(() =>
            {
                Parallel.For(0, 10000, x =>
                {
                    lock (lockObj)
                    {
                        TestValueA = x;
                        TestValueB = x;
                        m_IsFinishOnce = TestValueA >= TestValueB;
                    }
                });
                Debug.Log("运行完成");
            });

        This is actually a very common lock usage. As in the sample code above, there will never be an abnormal value. The value of m_IsFinishOnce will always be true. Similar to the Lock statement, there is another similar way of writing:

Monitor.Enter(lockObj);
TestValueA = x;
TestValueB = x;
m_IsFinishOnce = TestValueA >= TestValueB;
Monitor.Exit(lockObj);

Monitor Class (System.Threading) | Microsoft Learn Provides a mechanism for synchronizing access to objects. icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.monitor?view=netstandard-2.1         Simply put, Lock is a shortcut to Monitor, and any Monitor that can be implemented by Lock can also be implemented. If you want to know more about these two, you can read the following links:

Lock VS Monitor - Zhihu Introduction It is very important for developers to deal with multi-threaded applications with critical code sections. Monitor and lock are methods to provide thread safety in multi-threaded applications in c# language (the essence of the lock keyword is the encapsulation of Monitor). Both provide a mechanism to ensure that only… https://zhuanlan.zhihu.com/p/553789674

5.2. Mutex lock

        Lock or Mutex can only lock a single process. After all, the class we lock is only created in a single process. If multiple processes compete for the same resource (for example, they are all writing to the same document), an error will be reported. At this point we need Mutex to create a kernel-level application lock.

Mutex Class (System.Threading) | Microsoft Learn A synchronization primitive that can also be used for inter-process synchronization. icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netstandard-2.1         usage is also very simple;

private static Mutex mutex = new Mutex(false, "TestMutex");
……
mutex.WaitOne();
TestValueA = x;
TestValueB = x;
m_IsFinishOnce = TestValueA >= TestValueB;
mutex.ReleaseMutex();

        However, I feel that in general game development is a single-process logic, and mutexes are rarely needed. It may be used more often in the process of engineering pipeline development.

        Locks and Mutexes can only be released from the thread that acquired them.

5.3, signal lights

        (It’s called a semaphore in the book , but it’s called a semaphore in Microsoft’s documentation . Here it’s called a semaphore, and it’s called a semaphore hereafter.)

        Lock, Monitor, Mutex allow only one thread to access the protected resource. But sometimes we also need multiple processes to be able to access shared resources. For example, resource pooling (Resource Pooling) and throttling (Throttling) application scenarios need to allow multiple threads to access shared resources at the same time.

        Unlike Lock or Mutex, semaphore (Semaphore) is thread agnostic, which means that any thread can call the release of Semaphore. Semaphores also work across processes.

Semaphore Class (System.Threading) | Microsoft LearnLimits the number of threads that can simultaneously access a resource or resource pool. icon-default.png?t=N658https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.semaphore?view=netstandard-2.1         The sample code is as follows:

        private static Semaphore semaphore = new Semaphore(1, 3);//初始资源量,资源总量
        public static void RunWithSemaphore()
        {
            Task.Run(() =>
            {
                Parallel.For(0, 10, async x =>
                {
                    Debug.Log($"【{x} 】进入运行!");
                    semaphore.WaitOne();
                    await Task.Delay(1000);
                    semaphore.Release();
                    Debug.LogError($"【{x}】 完成运行!");
                });
                Debug.Log("全部运行完成");
            });
        }

        There are two parameters passed in by Semaphore, one is the initial amount of resources, and the other is the total amount of resources. The above code is actually waiting one by one, and executing a task every 1s:

         If initialCount is set to 2, this is two tasks at a time. This principle is actually the meaning of PV operation , which dynamically controls the blocking and execution of threads through signal lights.

Global Semaphore:

        Global to the operating system, kernel-level locking primitives are applied. Any semaphore created with a name (named when created) will be created as a global semaphore, otherwise as a local semaphore.


(to be continued)

 This tutorial corresponds to Learning Engineering: Magician Dix / HandsOnParallelProgramming · GitCode

Guess you like

Origin blog.csdn.net/cyf649669121/article/details/131681930