Mutex, spin lock, read-write lock, pessimistic lock, optimistic lock

Lock it if you don't agree!


The following article is from the wechat public account Kobayashi coding, the author Kobayashi coding

Mutual exclusion locks and spin locks: who is more comfortable

The bottom two are "mutual exclusion locks and spin locks". Many advanced locks are implemented based on them. You can think of them as the foundation of various locks, so we must be clear about the difference between the two. And application.

The purpose of locking is to ensure that the shared resource is accessed by only one thread at any time, so as to avoid the problem of shared data confusion caused by multiple threads.

When one thread has locked, other threads will fail to lock. The mutual exclusion lock and spin lock handle differently after locking failure:

  • After the mutex lock fails, the thread will release the CPU and give it to other threads;
  • After the spin lock fails to lock, the thread will be busy waiting until it obtains the lock; the
    mutex is a kind of "exclusive lock", for example, when thread A successfully locks, the mutex has been monopolized by thread A at this time , As long as thread A does not release the lock in its hand, thread B will fail to lock the lock, so it will release the CPU and give it to other threads. Since thread B releases the CPU, natural thread B's lock code will be blocked.

The phenomenon of mutex lock failure and blocking is realized by the operating system kernel. When the lock fails, the kernel will put the thread into a "sleep" state. After the lock is released, the kernel will wake up the thread at an appropriate time. When the thread successfully acquires the lock, it can continue execution. As shown in the following figure:
Insert picture description hereTherefore, when the mutex lock fails, it will fall into the kernel mode from the user mode, and let the kernel switch threads for us. Although the difficulty of using the lock is simplified, there is a certain performance overhead cost.

So what is this overhead cost? There will be two costs of thread context switching:

When the thread fails to lock, the kernel will set the thread state from the "running" state to the "sleep" state, and then switch the CPU to other threads to run;
then, when the lock is released, the thread in the previous "sleeping" state will be Change to the "ready" state, and then the kernel will switch the CPU to the thread to run at an appropriate time.
What is the context switch of the thread? When two threads belong to the same process, because the virtual memory is shared, the resources of the virtual memory remain intact when switching, and only the private data, registers and other unshared data of the threads need to be switched.

The time it takes to switch up and down has been counted by the big guys. It is probably between tens of nanoseconds to a few microseconds. If the execution time of your locked code is relatively short, then the context switching time may be longer than the execution time of your locked code. It's even longer.

Therefore, if you can be sure that the execution time of the locked code is very short, you should not use a mutex lock, but a spin lock, otherwise use a mutex lock.

Spin locks use the CAS function (Compare And Swap) provided by the CPU to complete the locking and unlocking operations in the "user mode" without actively generating thread context switching, so compared to mutex locks, it will be faster and costly Also smaller.

The general locking process consists of two steps:

  • The first step is to check the status of the lock. If the lock is free, perform the second step;
  • The second step is to set the lock to be held by the current thread;

The CAS function merges these two steps into a hardware-level instruction to form 原子指令, which ensures that these two steps are inseparable. Either the two steps are executed at once, or neither of the steps are executed.

When using spin locks, when multi-threaded competition for locks occurs, the thread that failed to lock will be "busy waiting" until it obtains the lock. Here the "busy-wait" can whilecycle wait to achieve, but it is best to use the CPU to provide PAUSEinstructions to implement the "busy-wait", because it can reduce power consumption during the cycle to wait.

A spin lock is the simplest type of lock. It keeps spinning and uses CPU cycles until the lock is available.需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。

Spin lock has low overhead. Generally, thread switching will not occur actively in multi-core systems. It is suitable for asynchronous, coroutine and other programming methods that switch requests in user mode. However, if the execution time of the locked code is too long, the spinning thread will It takes up CPU resources for a long time, so the spin time and the locked code execution time are in a "proportional" relationship. We need to know this clearly.

The use level of spin locks and mutual exclusion locks is similar, but the implementation level is completely different:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。

These two are the most basic processing methods of locks, and more advanced locks will choose one of them to implement. For example, read-write locks can be implemented by mutex locks or based on spin locks.

Read-write lock: Is there a priority distinction between read and write?

From the literal meaning of the read-write lock, we can also know that it is composed of two parts: a "read lock" and a "write lock". If you only read shared resources, use a "read lock" to lock, and if you want to modify shared resources, use a "write lock". "Locked.

and so,读写锁适用于能明确区分读操作和写操作的场景。

The working principle of the read-write lock is:

When the "write lock" is not held by a thread, multiple threads can hold read locks concurrently, which greatly improves the access efficiency of shared resources. Because the "read lock" is used to read shared resources, so many A thread holding a read lock at the same time will not destroy the data of the shared resource.
However, once the "write lock" is held by the thread, the read lock acquisition operation of the reader thread will be blocked, and the write lock acquisition operation of other write threads will also be blocked.
Therefore, the write lock is an exclusive lock, because only one thread can hold the write lock at any time, similar to mutexes and spin locks, while the read lock is a shared lock because the read lock can be held by multiple threads at the same time.

After knowing the working principle of the read-write lock, we can find that读写锁在读多写少的场景,能发挥出优势。

In addition, according to different implementations, read-write locks can be divided into "read priority locks" and "write priority locks".

The expectation of the read priority lock is that the read lock can be held by more threads in order to improve the concurrency of the reader thread. It works as follows: when the reader thread A first holds the read lock, the writer thread B is acquiring the write lock At the time, it will be blocked, and in the blocking process, the subsequent reader thread C can still successfully acquire the read lock. Finally, the writer thread B can successfully acquire the read lock after the reader threads A and C release the read lock. As shown in the figure below:
Insert picture description here
The write priority lock is the priority to serve the write thread, and its working method is: when the reader thread A first holds the read lock, the writer thread B will be blocked when acquiring the write lock, and in the blocking process, subsequent The incoming reader thread C will fail when acquiring the read lock, so the reader thread C will be blocked in the operation of acquiring the read lock, so as long as the reader thread A releases the read lock, the write thread B can successfully acquire the read lock. As shown in the figure below:
Insert picture description hereRead priority lock is better for reading thread concurrency, but it is not without problems. Let's imagine that if a reader thread always acquires a read lock, then the writer thread will never obtain the write lock, which causes the "starvation" phenomenon of the writer thread.

Write-first locks can ensure that the writer thread will not starve to death, but if there is always a writer thread acquiring the write lock, the reader thread will also be "starved to death".

Since the other party may starve to death regardless of the priority read lock or write lock, then we do not favor any party and engage in a "fair read-write lock".

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。

Both mutex locks and spin locks are the most basic locks. Read-write locks can be implemented by choosing one of these two locks according to the scenario.

Optimistic lock and pessimistic lock: What is the difference between the mentality of doing things?

The mutexes, spin locks, and read-write locks mentioned earlier are all pessimistic locks.

Pessimistic lock is more pessimistic, it thinks多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

On the contrary, if the probability of multiple threads modifying shared resources at the same time is relatively low, optimistic locking can be used.

Optimistic locking is more optimistic. It assumes that the probability of conflict is very low. It works as follows:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

How to retry after giving up is closely related to the business scenario. Although the cost of retrying is high, it is acceptable if the probability of conflict is low enough.

It can be seen that the mentality of Optimistic Lock is that, regardless of the three-seven-one, change the resources first. In addition, you will find乐观锁全程并没有加锁,所以它也叫无锁编程。

Here is an example of a scenario: online documentation.

We all know that online documents can be edited by multiple people at the same time. If the pessimistic lock is used, then as long as one user is editing the document, other users cannot open the same document at this time. Of course, the user experience is not good.

Realizing simultaneous editing by multiple people actually uses optimistic lock, which allows multiple users to open the same document for editing, and verify whether the modified content conflicts after editing and submitting.

What counts as a conflict? Here is an example. For example, user A first edits the document in the browser, and then user B opens the same document in the browser for editing, but user B submits changes than user A. User A does not know this process. When A submits the modified content, then there will be conflicts in the parallel modification between A and B.

How does the server verify if there is a conflict? The usual scheme is as follows:

  • Because the probability of conflict is relatively low, let the user edit the document first, but the browser will record the document version number returned by the server when downloading the document;
    when the user submits the modification, the request sent to the server will bring the original document version After receiving the number, the server compares it with the current version number. If the version number is the same, the modification is successful, otherwise the submission fails.
  • In fact, our common SVN and Git also use the idea of ​​optimistic locking. First, let the user edit the code, and then when submitting, use the version number to determine whether there is a conflict. If a conflict occurs, we need to modify it ourselves. Resubmit again.

Although optimistic locking removes the operation of locking and unlocking, once a conflict occurs, the cost of retry is very high, so只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

to sum up

During the development process, the most common one is the mutex lock. When the mutex lock fails, it will use "thread switching" to deal with it. When the thread that failed to lock the lock succeeds again in this process, there will be two The cost of secondary thread context switching, performance loss is relatively large.

If we clearly know that the execution time of the locked code is very short, then we should choose a spin lock with a relatively small overhead, because when the spin lock fails to lock, it will not actively generate thread switching, but has been busy waiting. Until the lock is acquired, if the execution time of the locked code is short, the busy waiting time is correspondingly short.

If you can distinguish between read and write operations, the read-write lock is more appropriate. It allows multiple reader threads to hold the read lock at the same time, which improves the concurrency of reads. According to the preference of the reader or the writer, it can be divided into a read priority lock and a write priority lock. The read priority lock is very concurrency, but the write thread will be starved to death, and the write priority lock will give priority to the write thread, and the read thread may also To be starved to death, in order to avoid the problem of starvation, there is a fair read-write lock, which uses a queue to queue the threads requesting the lock, and guarantees the principle of first-in, first-out to lock the threads, which guarantees a certain Threads will not be starved to death, and the versatility is better.

Both mutex locks and spin locks are the most basic locks. Read-write locks can be implemented by choosing one of these two locks according to the scenario.

In addition, mutexes, spin locks, and read-write locks are all pessimistic locks. Pessimistic locks believe that when concurrently accessing shared resources, the probability of conflict may be very high, so before accessing shared resources, you need to lock first.

On the contrary, if the conflict probability is very low when accessing shared resources concurrently, optimistic locking can be used. Its working method is that when accessing shared resources, there is no need to lock first. After modifying the shared resources, verify this period of time. Whether there is a conflict in it, if no other thread is modifying the resource, then the operation is completed. If it is found that other threads have modified the resource, the operation is abandoned.

However, once the conflict probability rises, it is not suitable to use optimistic locking, because the retry cost of resolving conflicts is very high.

No matter what kind of lock is used, the range of our locking code should be as small as possible, that is, the granularity of locking should be small, so that the execution speed will be faster. Then, if you use the right lock, it will speed up.

Guess you like

Origin blog.csdn.net/weixin_38640052/article/details/108632310
Recommended