In a multithreaded application, the programmer uses a mutex (mutex) to synchronize the thread's behavior into the area of code that can access the shared resource. The area of code protected by these locks is called the Critical Code snippet (Critical section). If a thread already exists in the critical code snippet, no other thread is allowed to enter the code snippet.
Threads should minimize the time spent in critical code snippets, which in turn reduces the time that other threads are idle waiting outside the code segment to acquire locks. But it is not possible to blindly divide a lot of small code snippets.
Example 1
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL Section 1
UpdateSharedData1 ()
END CRITICAL Section 1
DOFUNC1 ()
BEGIN CRITICAL Section 2
UPDATESHAREDDATA2 ()
END CRITICAL Section 2
DOFUNC2 ()
End Thread Function ()
In the above example, the key code snippet is separated by the DOFUNC1 () function.
This is worthwhile if the thread spends a long time in DoFunc1 ().
But if the thread spends a short time in DoFunc1 (), the better solution is to merge two small-key snippets into one key snippet
Example 2
Begin Thread Function ()
Initialize ()
BEGIN CRITICAL Section 1
UpdateSharedData1 ()
DOFUNC1 ()
UPDATESHAREDDATA2 ()
END CRITICAL Section 1
DOFUNC2 ()
End Thread Function ()
In Example 1, the key code snippet is separated by the DOFUNC1 () function. UpdateSharedData1 () and UPDATESHAREDDATA2 () are synchronized by two locks respectively. In Example 2, the two small-key snippets are combined into a large, critical section of code that contains the synchronization-independent function DoFunc1 ().
So which option is better?
It depends on the circumstances.
Example 1 performs better if the thread spends a long time in DoFunc1 (). Because in Example 2, a thread gets a lock, it wastes a lot of time in the critical code to functions unrelated to synchronization.
Example 2 performs better if the thread spends a short time in DoFunc1 (). Example 1: To reduce the time of DoFunc1 () in a critical code segment, the lock contention overhead of synchronizing two critical sections of code is borne. If the time of DoFunc1 () is less than 1 lock contention overhead, then example 1 loses performance.
Example 1 performs better if the thread takes a long time on the UPDATESHAREDDATA2 function. Because of this scenario, threads inevitably take a long time in the critical code snippet. In Example 2, when a thread enters the UPDATESHAREDDATA2 function, all other threads are blocked waiting to enter the UPDATESHAREDDATA1 function. Therefore, we can use Example 1, let the other line enters upgradeable finish processing the UPDATESHAREDDATA1 function.
Try to correlate locks to specific shared data. You should not create a separate lock for each element in the structure that shares the data, nor should you create a single lock to protect access to the entire structure. The optimal size of the lock should be between the two and you need to grasp it.
For the last case above (if the thread takes a long time on the UPDATESHAREDDATA2 function)
- The data structure accessed by the UPDATESHAREDDATA2 function is divided into two parts, each using a mutual exclusion lock. The UPDATESHAREDDATA2 function is then decomposed into two functions. Reduce lock contention by separating critical code.
- Analyzing the UPDATESHAREDDATA2 function, if the UPDATESHAREDDATA2 function does not need to protect the entire execution process, you might consider inserting the key snippet in the function that requires access to the shared data, rather than enclosing the entire function call.
Adjusts the size of the critical code snippet based on the cost of acquiring and releasing locks. The things we can do are:
- Consolidate small critical code snippets to share the locking overhead.
- Divides large, critical code segments with severe lock contention into smaller, critical sections of code.
- Locks are associated to specific shared data to minimize lock contention issues. The best solution might be to create a lock for each shared data element and create a lock for all shared data between the two extremes.
The use of large-key code snippets means that the algorithm itself is very low in concurrency, or that data partitioning between threads is not ideal. For the former, only the algorithm can be changed. For the latter, you can try to create a local copy of the shared data, which supports thread asynchronous access.
If we consider the cost of context switching, then we should decide whether to use a mutex or a spin lock. A spin lock is a wait loop that will always occupy the CPU, so there is no context switch. For threads that are waiting to enter a small critical snippet, using a spin lock may have higher performance than a mutex. However, because the waiting thread will still consume CPU resources in the rotation wait loop, it is recommended to use a rotation wait loop only if the thread spends very little time in the critical code segment and the adverse effect is lower than the environment switch.
In support of Intel? Hyper-Threading Technology (Intel? HT technology), a two-way logical processor is created on the same CPU core. Rotating threads and threads that are performing useful tasks are sure to scramble for logical processor resources. Rotating threads have a greater impact on the performance of multithreaded applications in systems with Intel Hyper-Threading technology than symmetric multiprocessor systems. In this case, the spin count of the spin lock should be lowered, or the spin lock should not be used.
Linux--Managing lock contention (translation)