青瑄 發表於 2020-12-17 15:09:00

Go TryLock实现

<h1 id="go-trylock实现">Go TryLock实现</h1>
<p>Go标准库的<code>sync/Mutex</code>、<code>RWMutex</code>实现了<code>sync/Locker</code>接口, 提供了<code>Lock()</code>和<code>UnLock()</code>方法,可以获取锁和释放锁,我们可以方便的使用它来控制我们对共享资源的并发控制上。</p>
<p>但是标准库中的<code>Mutex.Lock</code>的锁被获取后,如果在未释放之前再调用<code>Lock</code>则会被阻塞住,这种设计在有些情况下可能不能满足我的需求。有时候我们想尝试获取锁,如果获取到了,没问题继续执行,如果获取不到,我们不想阻塞住,而是去调用其它的逻辑,这个时候我们就想要<code>TryLock</code>方法了。</p>
<h3 id="使用-unsafe-操作指针">使用 <code>unsafe</code> 操作指针</h3>
<p>如果你查看<code>sync/Mutex</code>的代码,会发现<code>Mutext</code>的数据结构如下所示:</p>
<pre><code class="language-go">type Mutex struct {
        state int32
        semauint32
}
</code></pre>
<p>它使用<code>state</code>这个32位的整数来标记锁的占用,所以我们可以使用<code>CAS</code>来尝试获取锁。</p>
<p>代码实现如下:</p>
<pre><code class="language-go">const mutexLocked = 1 &lt;&lt; iota

type Mutex struct {
        sync.Mutex
}

func (m *Mutex) TryLock() bool {
        return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&amp;m.Mutex)), 0, mutexLocked)
}
</code></pre>
<p>使用起来和标准库的<code>Mutex</code>用法一样。</p>
<pre><code class="language-go">func main() {
        var m Mutex

        m.Lock()

        go func() {
                m.Lock()
        }()

        time.Sleep(time.Second)
        fmt.Printf("TryLock: %t\n", m.TryLock()) //false
        fmt.Printf("TryLock: %t\n", m.TryLock()) // false
        m.Unlock()
        fmt.Printf("TryLock: %t\n", m.TryLock()) //true
        fmt.Printf("TryLock: %t\n", m.TryLock()) //false
        m.Unlock()
        fmt.Printf("TryLock: %t\n", m.TryLock()) //true
        m.Unlock()
}
</code></pre>
<p>注意<code>TryLock</code>不是检查锁的状态,而是<strong>尝试获取</strong>锁,所以<code>TryLock</code>返回true的时候事实上这个锁已经被获取了。</p>
<h3 id="实现自旋锁">实现自旋锁</h3>
<p>上面一节给了我们启发,利用 <code>uint32</code>和<code>CAS</code>操作我们可以一个自定义的锁:</p>
<pre><code class="language-go">type SpinLock struct {
        f uint32
}

func (sl *SpinLock) Lock() {
        for !sl.TryLock() {
                runtime.Gosched()
        }
}

func (sl *SpinLock) Unlock() {
        atomic.StoreUint32(&amp;sl.f, 0)
}

func (sl *SpinLock) TryLock() bool {
        return atomic.CompareAndSwapUint32(&amp;sl.f, 0, 1)
}
</code></pre>
<p>整体来看,它好像是标准库的一个精简版,没有休眠和唤醒的功能。</p>
<p>当然这个自旋锁可以在大并发的情况下CPU的占用率可能比较高,这是因为它的<code>Lock</code>方法使用了自旋的方式,如果别人没有释放锁,这个循环会一直执行,速度可能更快但CPU占用率高。</p>
<p>当然这个版本还可以进一步的优化,尤其是在复制的时候。下面是一个优化的版本:</p>
<pre><code class="language-go">type spinLock uint32

func (sl *spinLock) Lock() {
        for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
                runtime.Gosched() //without this it locks up on GOMAXPROCS &gt; 1
        }
}

func (sl *spinLock) Unlock() {
        atomic.StoreUint32((*uint32)(sl), 0)
}

func (sl *spinLock) TryLock() bool {
        return atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1)
}

func SpinLock() sync.Locker {
        var lock spinLock
        return &amp;lock
}
</code></pre>
<h3 id="使用-channel-实现">使用 channel 实现</h3>
<p>另一种方式是使用channel:</p>
<pre><code class="language-go">type ChanMutex chan struct{}

func (m *ChanMutex) Lock() {
        ch := (chan struct{})(*m)
        ch &lt;- struct{}{}
}

func (m *ChanMutex) Unlock() {
        ch := (chan struct{})(*m)
        select {
        case &lt;-ch:
        default:
                panic("unlock of unlocked mutex")
        }
}

func (m *ChanMutex) TryLock() bool {
        ch := (chan struct{})(*m)
        select {
        case ch &lt;- struct{}{}:
                return true
        default:
        }
        return false
}
</code></pre>
<p>有兴趣的同学可以关注我的同事写的库 lrita/gosync。</p>
<h3 id="性能比较">性能比较</h3>
<p>首先看看上面三种方式和标准库中的<code>Mutex</code>、<code>RWMutex</code>的<code>Lock</code>和<code>Unlock</code>的性能比较:</p>
<pre><code>BenchmarkMutex_LockUnlock-4                 100000000                16.8 ns/op             0 B/op             0 allocs/op
BenchmarkRWMutex_LockUnlock-4               50000000                36.8 ns/op             0 B/op             0 allocs/op
BenchmarkUnsafeMutex_LockUnlock-4           100000000                16.8 ns/op             0 B/op             0 allocs/op
BenchmarkChannMutex_LockUnlock-4            20000000                65.6 ns/op             0 B/op             0 allocs/op
BenchmarkSpinLock_LockUnlock-4              100000000                18.6 ns/op             0 B/op             0 allocs/op
</code></pre>
<p>可以看到单线程(goroutine)的情况下 <code>spinlock</code>并没有比标准库好多少,反而差一点,并发测试的情况比较好,如下表中显示,这是符合预期的。</p>
<p><code>unsafe</code>方式和标准库差不多。</p>
<p><code>channel</code>方式的性能就比较差了。</p>
<pre><code>BenchmarkMutex_LockUnlock_C-4                 20000000                75.3 ns/op             0 B/op             0 allocs/op
BenchmarkRWMutex_LockUnlock_C-4               20000000             100 ns/op             0 B/op             0 allocs/op
BenchmarkUnsafeMutex_LockUnlock_C-4           20000000                75.3 ns/op             0 B/op             0 allocs/op
BenchmarkChannMutex_LockUnlock_C-4            10000000             231 ns/op             0 B/op             0 allocs/op
BenchmarkSpinLock_LockUnlock_C-4              50000000                32.3 ns/op             0 B/op             0 allocs/op
</code></pre>
<p>再看看三种实现<code>TryLock</code>方法的锁的性能:</p>
<pre><code>BenchmarkUnsafeMutex_Trylock-4              50000000                34.0 ns/op             0 B/op             0 allocs/op
BenchmarkChannMutex_Trylock-4                 20000000                83.8 ns/op             0 B/op             0 allocs/op
BenchmarkSpinLock_Trylock-4                 50000000                30.9 ns/op             0 B/op             0 allocs/op
</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    Songzhibin<br><br>
来源:https://www.cnblogs.com/binHome/p/14149750.html
頁: [1]
查看完整版本: Go TryLock实现