golang基于软件事务内存(STM)的并发控制插件库stl的使用

Golang基于软件事务内存(STM)的并发控制插件库stl的使用

软件事务锁

Alt text

stl包提供了基于软件事务内存(STM)并发控制机制的多重原子动态共享/排他锁。此外,stl锁可以接受context.Context,允许取消或设置锁定操作的截止时间。

锁快速且轻量级。实现只需要每个vault(包含任意数量资源的锁集合)一个mutex和一个channel

安装

使用以下命令安装包:

go get github.com/ssgreg/stl

快速入门

stl可以原子地锁定任意数量的资源而不会死锁。每个资源可以以exclusive方式锁定(同一时间只有一个锁持有者可以锁定该资源)或以shared方式锁定(所有’shared’锁持有者可以同时锁定该资源,想要以’exclusive’方式锁定该资源的锁持有者将等待它们完成)。

您还可以在构建事务或事务锁时组合sharedexclusive资源:

// 一个包含所有锁定资源的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

1 回复

更多关于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)
	})
}

性能考虑

  1. 冲突处理:当多个事务冲突时,STM会自动重试事务,这可能导致性能下降
  2. 事务大小:保持事务尽可能小,减少冲突概率
  3. 重试策略:合理使用RetryRetryOnConflict

适用场景

STM特别适合以下场景:

  • 需要维护多个变量的原子性更新
  • 复杂的条件更新逻辑
  • 读多写少的并发场景

总结

Go的stm库提供了一种优雅的方式来处理并发编程中的共享状态问题。相比传统的锁机制,STM可以避免死锁问题,并且代码更加简洁。然而,它并不是万能的,在某些高冲突场景下性能可能不如精心设计的锁方案。

使用时需要根据具体场景权衡利弊,合理设计事务边界和重试策略。

回到顶部