首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
Go语言的面向对象编程
面向对象编程的本质
Go语言实现封装
Go语言中字段和方法的封装
为值类型和指针类型绑定方法的区别
Go语言实现继承
利用组合实现继承
匿名字段的支持
多继承
Go语言实现多态
面向接口编程
Go语言中的接口
Go语言中的接口实现
利用面向接口编程实现方法多态
编程范例——接口的典型应用
接口嵌套实例
伪继承与接口实现
并发
线程的概念
线程模型
协程的工作原理
协程的使用
GPM模型
从线程模型看GOMAXPROCS参数
Go语言中的协程同步
独占锁——Mutex
读写锁——RWMutex
等待组——WaitGroup
利用channel实现协程同步
利用channel实现锁定
利用channel实现等待组
总结使用channel实现并发控制
让出时间片
time.Sleep()和runtime.Gosched()的本质区别
runtime.Gosched()与多核CPU
Go语言中的单例
利用sync.Once实现单例
sync.Once的实现原理
编程范例——协程池及协程中断
协程池的实现
协程的中断执行
当前位置:
首页>>
技术小册>>
深入浅出Go语言核心编程(四)
小册名称:深入浅出Go语言核心编程(四)
### 章节标题:利用Channel实现锁定 在Go语言的并发编程模型中,`goroutine`和`channel`是两大核心基石。`goroutine`是Go语言对协程的实现,提供了轻量级的线程管理;而`channel`则用于在不同的`goroutine`之间安全地传递数据。虽然Go语言的标准库提供了诸如`sync`包中的互斥锁(`sync.Mutex`)和读写锁(`sync.RWMutex`)等显式锁机制来同步并发操作,但在某些场景下,巧妙地利用`channel`的特性也能实现类似锁定的效果,同时保持代码的简洁性和Go的并发哲学。 #### 一、Channel的基本特性与锁定需求 在深入探讨如何利用`channel`实现锁定之前,我们需要先理解`channel`的几个关键特性: 1. **阻塞特性**:默认情况下,发送操作(`send`)在接收方准备好之前会阻塞,反之亦然。这种阻塞行为可以用于控制`goroutine`的执行顺序。 2. **无缓冲与有缓冲**:无缓冲的`channel`在发送和接收之间直接传递数据,而不需要额外的存储空间;有缓冲的`channel`则允许在发送和接收之间暂存一定数量的数据。 3. **关闭与关闭通知**:当不再需要向`channel`发送数据时,可以关闭它。关闭后的`channel`仍可以接收数据,但无法再发送数据,且接收操作会在数据接收完毕后返回零值及一个非阻塞的关闭通知。 在并发编程中,锁定通常用于保护共享资源,防止多个`goroutine`同时访问导致的竞争条件(race condition)和数据不一致问题。传统的锁机制通过加锁和解锁操作来控制对共享资源的访问,而利用`channel`实现锁定则是通过`channel`的阻塞特性和协程调度机制来达到同步访问的目的。 #### 二、利用无缓冲Channel实现互斥锁 无缓冲的`channel`由于其阻塞特性,可以很方便地用于实现简单的互斥锁。当一个`goroutine`需要访问共享资源时,它会尝试向一个无缓冲的`channel`发送一个信号(可以是任何值,因为重点是阻塞),这个操作会立即阻塞,直到另一个`goroutine`完成资源的访问并从`channel`中接收了这个信号。 ##### 示例代码 ```go package main import ( "fmt" "sync" "time" ) type MutexChannel struct { lock chan struct{} } func NewMutexChannel() *MutexChannel { return &MutexChannel{ lock: make(chan struct{}, 0), // 无缓冲Channel } } func (m *MutexChannel) Lock() { m.lock <- struct{}{} // 发送信号,阻塞等待 } func (m *MutexChannel) Unlock() { <-m.lock // 接收信号,解锁 } func main() { var wg sync.WaitGroup mutex := NewMutexChannel() for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() mutex.Lock() fmt.Printf("Goroutine %d is accessing the resource\n", id) time.Sleep(time.Second) // 模拟资源访问耗时 mutex.Unlock() }(i) } wg.Wait() fmt.Println("All goroutines have completed access to the resource.") } ``` 在上述示例中,`MutexChannel`结构体封装了一个无缓冲的`channel`,并通过`Lock`和`Unlock`方法模拟了锁的加锁和解锁操作。每个`goroutine`在访问共享资源前必须先调用`Lock`方法,这会尝试向`lock`通道发送一个空结构体(不占用额外空间),如果此时通道已满(无缓冲,因此总是“满”的),则发送操作会阻塞,直到另一个`goroutine`调用`Unlock`方法并从通道中接收数据,从而解锁。 #### 三、利用有缓冲Channel实现信号量 虽然利用无缓冲`channel`可以实现简单的互斥锁,但有时候我们可能需要更复杂的同步机制,比如信号量(Semaphore),用于控制同时访问共享资源的`goroutine`数量。信号量允许指定数量的`goroutine`同时进入临界区,超出数量的`goroutine`将被阻塞,直到有`goroutine`退出临界区并释放信号量。 ##### 示例代码 ```go package main import ( "fmt" "sync" "time" ) type Semaphore struct { slots chan struct{} } func NewSemaphore(size int) *Semaphore { return &Semaphore{ slots: make(chan struct{}, size), } } func (s *Semaphore) Acquire() { s.slots <- struct{}{} // 尝试获取一个空位 } func (s *Semaphore) Release() { <-s.slots // 释放一个空位 } func main() { var wg sync.WaitGroup sem := NewSemaphore(3) // 允许同时3个goroutine访问 for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() sem.Acquire() fmt.Printf("Goroutine %d entered the critical section\n", id) time.Sleep(time.Second) // 模拟资源访问耗时 sem.Release() fmt.Printf("Goroutine %d left the critical section\n", id) }(i) } wg.Wait() fmt.Println("All goroutines have completed.") } ``` 在这个例子中,`Semaphore`结构体使用了一个有缓冲的`channel`作为信号量的实现。`Acquire`方法尝试向`slots`通道发送一个空结构体,如果通道未满(即还有空位),则发送成功,`goroutine`进入临界区;如果通道已满,则发送操作阻塞,等待其他`goroutine`调用`Release`方法并从通道中接收数据,从而释放一个空位。`Release`方法则通过从`slots`通道接收数据来释放一个空位,允许其他等待的`goroutine`进入临界区。 #### 四、总结 通过巧妙地利用`channel`的阻塞特性和协程调度机制,我们可以实现类似于传统锁机制的同步控制,同时保持Go语言并发编程的简洁性和高效性。无论是简单的互斥锁还是复杂的信号量,`channel`都提供了一种灵活且强大的同步手段。当然,在实际开发中,我们应根据具体需求选择合适的同步机制,以达到最佳的并发性能和资源利用率。
上一篇:
利用channel实现协程同步
下一篇:
利用channel实现等待组
该分类下的相关小册推荐:
深入浅出Go语言核心编程(八)
Go Web编程(中)
Go语言从入门到实战
企业级Go应用开发从零开始
深入浅出Go语言核心编程(三)
Golang并发编程实战
Go Web编程(下)
深入浅出Go语言核心编程(五)
深入浅出Go语言核心编程(二)
深入浅出Go语言核心编程(一)
Go Web编程(上)
深入解析go语言