Exploring the distinctions between locks and reentrant locks, understanding the unique stance of Golang, and navigating the complexities of reentrant locks in systems design.
We make a living by what we get, but we make a life by what we give.
— Winston Churchill
Medium Link: Deep Reflection on Reentrant Locks in relation to Golang | by Wesley Wei | May, 2024 | Programmer’s Career
1.1 Lock and Recursive Lock
Explain the difference between locks and reentrant locks, and how they work?
Locks are a concept in concurrency control. When a thread A already holds a lock, when thread B tries to enter the code section protected by this lock, it will be blocked. This shows that the operation granularity of locks is “threads” rather than calls.
The term “recursive” refers to a thread already holding a lock being able to acquire the same lock again without causing deadlock. This means that the same thread can re-enter the synchronized code section using the lock it has already acquired, which is a recursive lock.
Recursive locks are mainly used when a thread needs to enter the critical section multiple times. This requires the lock to be recursive, also known as a recursive lock.
1.2 Golang does not support Reentrant Lock
Golang does not support reentrant locks, and some code examples to help understand this are provided.
In Java, both synchronized locks and ReentrantLocks are recursive locks. The following code will not have any problems.
1 | import java.util.concurrent.locks.Lock; |
However, in Go, the same code will result in deadlock!
1 | package main |
Run it: Better Go Playground
Let’s dive deeper into read-write locks from a read-write perspective: Go only supports mutex locks, and all the characteristics of read-write locks are as follows:
- Read and read operations are not mutually exclusive
- Read and write, write and write operations are mutually exclusive
Since read locks can be acquired multiple times, read locks must be recursive, which is not explicitly demonstrated here. However, read locks and write locks are mutually exclusive, and Go does not support recursive locks, which can cause problems for people who are accustomed to Java and other languages when using Go. The following code demonstrates a potential problem.
1 | package main |
Run it: Better Go Playground
This code executes in order of ①, ②, ③, and the second write lock needs to wait for the first read lock to be released, and the third read lock needs to wait for the second write lock to be released, resulting in a deadlock logic.
In Java, this same logic will not have any problems, and interested readers can try it themselves. At this point, some people may have doubts: Go does this in a reasonable way? Why is it implemented like this? After all, from a direct perspective:
- A thread (or line) already holds a read lock, and another thread (or line) acquires a write lock, it must wait for the release of the read lock. Since this thread (or line) already holds the read lock, why does it need to manage the waiting of the write lock again?
This requires us to understand the principles behind Golang’s exclusion of reentrant locks.
1.3 Golang’s Philosophy on Excluding Reentrant Locks
Why does Golang exclude support for reentrant locks?
To answer this question, we first need to understand the design principles of Golang’s mutex locks. Golang follows these principles when designing mutex locks:
- When calling
mutex.Lock()
, the variables’ invariants must be preserved and not be altered in subsequent processes. - When calling
mu.Unlock()
, the following conditions must be met:- The program no longer requires the dependence on those variables.
- If the variables were altered during the mutex lock period, then it must be ensured that they have been restored.
Let’s consider an example from the perspective of reentrancy:
1 | func F() { |
In F()
, we acquire the lock using mu.Lock()
. If reentrant locks were supported, we would then enter G()
. However, this would lead to a fatal issue because we would not know whether F()
and G()
modified any variables after acquiring the lock. This would result in the violation of invariants.
Experimenting with GO provides further details, and the key points are as follows:
Recursive mutexes do not protect invariants.
Mutexes have only one job, and recursive mutexes don’t do it.
1.4 Implementing Reentrant Locks in Golang
Now that we understand the design principles of Golang’s mutex locks, let’s consider how we would implement reentrant locks.
To implement reentrant locks, we need to fulfill two requirements:
- Remember the goroutine holding the lock
- Keep track of the number of reentries
Tracking the number of reentries is relatively easy to implement. If we could bind the lock and the goroutine ID, we could satisfy the first requirement, and we could theoretically implement reentrant locks. However, I believe that there is no real need to implement reentrant locks for Golang, as at least the Go language team has not provided support for it.
I believe that for certain scenarios, using mutex locks would be more beneficial for the business than relying on “reentrant locks.” After all, reentrant locks could potentially introduce new issues.
1.5 Potential Issues with Reentrant Locks
Understand the potential issues that could influence Golang’s decision regarding reentrant locks, or pitfalls.
Potential issues:
- Performance degradation due to excessive lock nesting.
- Potential deadlock risks, such as forgetting to release the lock.
- Increased complexity in implementation and usage.
Therefore, when considering using reentrant locks, we need to carefully weigh their advantages and disadvantages and determine whether we truly require their reentrant feature.
In the world of Golang, I believe we should adhere to Golang’s design principles.
1.6 References
这不会又是一个Go的BUG吧?-腾讯云开发者社区-腾讯云
Go实现可重入锁的两种办法 - 掘金
Experimenting with GO
中文文章: https://programmerscareer.com/zh-cn/golang-reentry-lock/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: If you choose to repost or use this article, please cite the original source.
Comments