golang基于软件事务内存(STM)的并发控制插件库stl的使用
Golang基于软件事务内存(STM)的并发控制插件库stl的使用
软件事务锁
stl
包提供了基于软件事务内存(STM)并发控制机制的多重原子动态共享/排他锁。此外,stl
锁可以接受context.Context
,允许取消或设置锁定操作的截止时间。
锁快速且轻量级。实现只需要每个vault(包含任意数量资源的锁集合)一个mutex
和一个channel
。
安装
使用以下命令安装包:
go get github.com/ssgreg/stl
快速入门
stl
可以原子地锁定任意数量的资源而不会死锁。每个资源可以以exclusive
方式锁定(同一时间只有一个锁持有者可以锁定该资源)或以shared
方式锁定(所有’shared’锁持有者可以同时锁定该资源,想要以’exclusive’方式锁定该资源的锁持有者将等待它们完成)。
您还可以在构建事务或事务锁时组合shared
和exclusive
资源:
// 一个包含所有锁定资源的vault
v := stl.NewVault()
// ...
locker := stl.New().Exclusive("terminal").Shared("network").ToLocker(v)
locker.Lock()
defer locker.Unlock()
如果您想能够取消或设置锁定操作的截止时间,还可以调用locker.LockWithContext(ctx)
。这将为您的应用程序增加额外的灵活性。
示例:哲学家就餐问题
在计算机科学中,哲学家就餐问题是一个常用于并发算法设计中的示例问题,用于说明同步问题及其解决技术。值得注意的是,使用stl
,与任何其他"裸"解决方案相比,可以优雅地解决该任务。以下是问题的简要描述:
五位哲学家围坐在圆形餐桌旁。桌上有五盘意大利面和五把叉子,排列方式使每位哲学家可以用双手拿起离他最近的两把叉子。哲学家要么思考他们的哲学问题,要么吃意大利面。思想家们必须使用两把叉子才能进食。如果一位哲学家只拿着一把叉子,他就不能吃饭或思考。需要组织他们的存在方式,使每位哲学家可以轮流吃饭和思考,永远如此。
首先我们需要在我们的并发模型中表示叉子(资源)。为了区分不同的叉子,我们将为每个叉子分配一些标签。使用stl
不需要创建和保留资源本身。标签(或名称)就足够了。
// 五位哲学家每人两把叉子
resources := [][]string{
{"fork_1", "fork_2"},
{"fork_2", "fork_3"},
{"fork_3", "fork_4"},
{"fork_4", "fork_5"},
{"fork_5", "fork_1"},
}
让我们继续构建一个事务(或在这种情况下的事务锁)。当哲学家将他的活动从thinking
更改为eating
时,他试图以exclusive
方式拿起左右叉子。如果他成功获取(锁定)两者,他会花一些时间吃他的意大利面。如果任何叉子被邻居拿走,我们的哲学家应该等待任何其他哲学家完成进食(解锁)。
这是如何为每位哲学家创建事务锁的。
// 一个包含所有锁定资源的vault
v := stl.NewVault()
// ...
for n := 0; n < 5; n++ {
// ...
// 可以原子地独占锁定/解锁两把叉子的锁
locker := stl.New().Exclusive(resources[n][0]).Exclusive(resources[n][1]).ToLocker(v)
// ...
}
要更改哲学家的活动(独占锁定两个指定的资源),我们需要调用locker.Lock()
并在最后调用locker.Unlock()
。如果我们想能够取消或设置锁定操作的截止时间,因为等待其他参与者解锁使用的资源可能需要一些时间,也可以调用locker.LockWithContext(ctx)
。
// 哲学家在这里思考...
// ...
// 现在他决定拿起叉子吃一点意大利面
locker.Lock()
defer locker.Unlock()
// 哲学家在这里吃饭...
// ...
但现在我们必须停下来理解这个事务锁会做什么。假设两把叉子都是空闲的,那么锁将成功锁定它们所代表的两个资源。然而更有可能的是其中一把叉子已经被其他人拿走了。“全有或全无”,这个原则在STM机制中运作良好。如果两把叉子中的任何一把已经被拿走,事务将重新启动,直到锁成功锁定两个资源。我们可以认为,当且仅当锁的所有资源都被成功锁定时,事务才会成功。换句话说,如果某些叉子处于不希望的状态,事务锁将不会继续。
我们即将完成我们的解决方案。思考-进食函数:
do := func(locker Locker) {
// 思考300毫秒
time.Sleep(time.Millisecond * 300)
// 等待空闲的叉子
locker.Lock()
defer locker.Unlock()
// 进食100毫秒
time.Sleep(time.Millisecond * 100)
}
评估:
for n := 0; n < 5; n++ {
// 五位哲学家各自在自己的goroutine中生活
go func(n int) {
// 可以原子地独占锁定/解锁两把叉子的锁
locker := New().Exclusive(resources[n][0]).Exclusive(resources[n][1]).ToLocker(v)
// 思考-进食五次
for i := 0; i < 5; i++ {
do(locker)
}
}(n)
}
结论
stl
为您提供了一种方便的方式来构建可靠的无死锁应用程序。
完整示例代码
package main
import (
"fmt"
"time"
"github.com/ssgreg/stl"
)
func main() {
// 五位哲学家每人两把叉子
resources := [][]string{
{"fork_1", "fork_2"},
{"fork_2", "fork_3"},
{"fork_3", "fork_4"},
{"fork_4", "fork_5"},
{"fork_5", "fork_1"},
}
// 一个包含所有锁定资源的vault
v := stl.NewVault()
do := func(name string, locker stl.Locker, i int) {
// 思考300毫秒
fmt.Printf("%s is thinking %dth time\n", name, i+1)
time.Sleep(time.Millisecond * 300)
// 等待空闲的叉子
start := time.Now()
locker.Lock()
defer locker.Unlock()
starving := time.Since(start)
// 进食100毫秒
fmt.Printf("%s is eating %dth time, was starving for %v\n", name, i+1, starving)
time.Sleep(time.Millisecond * 100)
}
for n := 0; n < 5; n++ {
name := fmt.Sprintf("Philosopher %d", n+1)
// 可以原子地独占锁定/解锁两把叉子的锁
locker := stl.New().Exclusive(resources[n][0]).Exclusive(resources[n][1]).ToLocker(v)
// 每位哲学家在自己的goroutine中
go func(name string, locker stl.Locker) {
// 思考-进食五次
for i := 0; i < 5; i++ {
do(name, locker, i)
}
}(name, locker)
}
// 让哲学家们有时间完成他们的思考-进食循环
time.Sleep(3 * time.Second)
}
更多关于golang基于软件事务内存(STM)的并发控制插件库stl的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang基于软件事务内存(STM)的并发控制插件库stl的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Golang 基于软件事务内存(STM)的并发控制库 stm 使用指南
软件事务内存(Software Transactional Memory, STM)是一种并发控制机制,它借鉴了数据库事务的概念来管理共享内存的并发访问。在Go语言中,stm
是一个流行的STM实现库。
stm 库简介
stm
库提供了以下核心功能:
- 原子性:事务中的所有操作要么全部成功,要么全部失败
- 隔离性:事务执行过程中不会被其他事务干扰
- 轻量级:相比传统的锁机制,STM通常有更好的性能
安装
go get github.com/lukechampine/stm
基本用法
1. 创建和读写共享变量
package main
import (
"fmt"
"github.com/lukechampine/stm"
)
func main() {
// 创建共享变量
x := stm.NewVar(10)
y := stm.NewVar(20)
// 定义一个事务
transfer := func(tx *stm.Tx) {
// 读取当前值
currentX := tx.Get(x).(int)
currentY := tx.Get(y).(int)
// 修改值
tx.Set(x, currentX-5)
tx.Set(y, currentY+5)
}
// 执行事务
stm.Atomically(transfer)
// 读取最终结果
fmt.Println("x:", stm.AtomicGet(x)) // 输出: x: 5
fmt.Println("y:", stm.AtomicGet(y)) // 输出: y: 25
}
2. 条件事务
func transferWithCondition() {
accountA := stm.NewVar(100)
accountB := stm.NewVar(0)
// 只有当accountA余额足够时才转账
transfer := func(tx *stm.Tx) {
balance := tx.Get(accountA).(int)
if balance >= 50 {
tx.Set(accountA, balance-50)
tx.Set(accountB, tx.Get(accountB).(int)+50)
} else {
// 余额不足,可以重试或采取其他操作
tx.Retry()
}
}
// 执行事务
stm.Atomically(transfer)
fmt.Println("A:", stm.AtomicGet(accountA))
fmt.Println("B:", stm.AtomicGet(accountB))
}
3. 组合事务
func composeTransactions() {
balance := stm.NewVar(100)
deposit := func(amount int) stm.Operation {
return func(tx *stm.Tx) {
tx.Set(balance, tx.Get(balance).(int)+amount)
}
}
withdraw := func(amount int) stm.Operation {
return func(tx *stm.Tx) {
current := tx.Get(balance).(int)
if current < amount {
tx.Retry()
}
tx.Set(balance, current-amount)
}
}
// 组合多个操作
stm.Atomically(stm.Compose(
deposit(50),
withdraw(30),
))
fmt.Println("Final balance:", stm.AtomicGet(balance))
}
高级特性
1. 选择性重试
func selectiveRetry() {
var1 := stm.NewVar(10)
var2 := stm.NewVar(20)
op := func(tx *stm.Tx) {
v1 := tx.Get(var1).(int)
if v1 < 15 {
// 只有当var1变化时才重试
tx.RetryOnConflict(var1)
}
v2 := tx.Get(var2).(int)
tx.Set(var2, v2+v1)
}
stm.Atomically(op)
}
2. 嵌套事务
func nestedTransactions() {
account := stm.NewVar(100)
withdraw := func(amount int) stm.Operation {
return func(tx *stm.Tx) {
current := tx.Get(account).(int)
if current < amount {
tx.Retry()
}
tx.Set(account, current-amount)
}
}
deposit := func(amount int) stm.Operation {
return func(tx *stm.Tx) {
tx.Set(account, tx.Get(account).(int)+amount)
}
}
// 嵌套事务
stm.Atomically(func(tx *stm.Tx) {
stm.Atomically(withdraw(30), tx)
stm.Atomically(deposit(50), tx)
})
}
性能考虑
- 冲突处理:当多个事务冲突时,STM会自动重试事务,这可能导致性能下降
- 事务大小:保持事务尽可能小,减少冲突概率
- 重试策略:合理使用
Retry
和RetryOnConflict
适用场景
STM特别适合以下场景:
- 需要维护多个变量的原子性更新
- 复杂的条件更新逻辑
- 读多写少的并发场景
总结
Go的stm
库提供了一种优雅的方式来处理并发编程中的共享状态问题。相比传统的锁机制,STM可以避免死锁问题,并且代码更加简洁。然而,它并不是万能的,在某些高冲突场景下性能可能不如精心设计的锁方案。
使用时需要根据具体场景权衡利弊,合理设计事务边界和重试策略。