极客时间--golang并发编程实战课--Mutex学习总结

互斥锁的实现机制

  互斥锁是并发控制的一个手段,是为了避免竞争而建立的一种并发控制机制。在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。可以说,临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用,等等。如果很多线程同步访问临界区,就会造成访问或操作错误,这当然不是我们希望看到的结果。所以,我们可以使用互斥锁,限定临界区只能同时由一个线程持有。当临界区由一个线程持有的时候,其它线程如果想进入这个临界区,就会返回失败,或者是等待。直到持有的线程退出临界区,这些等待线程中的某一个才有机会接着持有这个临界区。

Mutex的使用方法

数据结构:

Mutex实现了如下接口,

type Locker interface{
    Lock()
    UnLock()
}

Mutex提供了如下两个方法:进入临界区调用Lock(),退出临界区调用UnLock()

func (m *Mutex)Lock()
func (m *Mutex)UnLock()

当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后,其它请求锁的goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。

下面来看一段代码:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex

type Mutext struct {
}

func main() {
	var value = 0
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				//lock.Lock()
				value++
				//lock.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(value)
}

上述代码中,value++就是一个临界区的概念,如果没有对其加锁,那么最终输出的value结果不是我们希望得到的,只有当对其加锁/解锁之后,才能保证在每次执行该操作只有一个goroutine(协程)。

同样,我们可以基于golang中结构体去优化上部分代码,谈不上优化,就是使用结构体去封装一个线程安全的计数器,可以将sync.Mutex作为一个字段,嵌入到结构体中去

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex

type Value struct {
	mu    sync.Mutex
	value int
}

func main() {
	var value Value
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < 1000; j++ {
				value.mu.Lock()
				value.value++
				value.mu.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println(value.value)
}

额外当前的 Mutex 最重要的变化,就是增加饥饿模式。第 12 行将饥饿模式的最大等待时间阈值设置成了 1 毫秒,这就意味着,一旦等待者等待的时间超过了这个阈值,Mutex 的处理就有可能进入饥饿模式,优先让等待者先获取到锁,通过加入饥饿模式,可以避免把机会全都留给新来的 goroutine,保证了请求锁的goroutine 获取锁的公平性,对于我们使用锁的业务代码来说,不会有业务一直等待锁不被处理。

posted @ 2023-06-04 12:45  99号的格调  阅读(22)  评论(0编辑  收藏  举报