我们平时会忽略掉的隐式的并发安全设计

2023-11-14 23:22:20 浏览数 (4)

什么是隐式的并发安全设计

隐式的并发安全设计是一种在代码中考虑并发性和安全性的方法,通过结构、模式和技术来减少竞态条件和其他并发问题的出现,而无需显式地使用锁或同步原语。这种设计方法旨在降低开发人员处理并发性问题的复杂性,同时提高代码的可读性、可维护性和性能。

实现隐式的并发安全设计的方法

  1. 不可变性(Immutability): 设计数据结构和对象,使其在创建后不可更改。通过避免共享可变状态,可以避免多线程并发修改造成的问题。
  2. 纯函数(Pure Functions): 使用纯函数,即函数的输出仅取决于输入参数,不依赖于外部状态。纯函数可以减少副作用,从而降低并发问题的可能性。
  3. 函数式编程(Functional Programming): 使用函数式编程范式,强调无副作用、不可变性和高阶函数的使用。这种方式可以鼓励编写更安全的并发代码。
  4. 数据隔离(Data Isolation): 将数据分割成不同的部分,每个部分受不同的锁或同步机制保护。这可以降低共享状态的风险,从而减少竞态条件。
  5. 消息传递(Message Passing): 使用消息传递模型,通过发送消息而不是共享数据来进行通信。这可以避免数据竞争,提高代码的可伸缩性。
  6. 异步编程(Asynchronous Programming): 使用异步编程模型,例如协程(Coroutine)或异步任务,来处理并发操作。这种方式可以减少显式的锁和线程同步,提高代码的响应性。
  7. 无锁数据结构(Lock-Free Data Structures): 使用无锁数据结构,这些数据结构设计为在没有锁的情况下支持并发访问。这可以提供更高的并发性能,但需要更高的技术水平来实现和维护。
  8. 函数闭包(Function Closures): 使用闭包来封装状态和行为,避免共享状态的直接访问。这有助于隔离并发操作。
  9. Actor 模型: 使用 Actor 模型来管理并发行为,其中每个 Actor 都是独立的实体,通过消息传递进行通信。这种模型可以在不使用显式锁的情况下处理并发。
  10. 软件事务内存(Software Transactional Memory,STM): STM 是一种并发控制方式,通过类似于数据库事务的方式来管理共享数据的并发访问。它可以减少锁的使用,但需要额外的处理来处理事务冲突。

但当我们在谈并发安全时,一般会谈到以下两种程序上的实现:

  1. 通过同步共享内存实现并发安全,比如使用锁
  2. 通过通信来实现同步操作,比如使用 channel

以上是显式实现并发安全的方案,但也许很多人忽视了,我们还可以通过隐式的数据保护实现并发安全。所谓隐式,就是通过巧妙的设计,使一个程序中原本存在的资源竞争问题不存在,消灭了问题的来源,也就不需要费力解决了。

实现隐式的并发安全,核心的思路是避免数据的变化。不可变的数据天然就是线程安全的。使用这样的程序设计,一方面可以降低开发者的认知负担,另一方面能获得更好的性能。

具体实现上,我们首先可以考虑声明常量而不是变量,Swift 可以使用 let,Java 可以使用 final,C 和 Go 可以使用 const。可惜的是,Go 不支持传递常量的指针,只能采用值拷贝的方式——但即便如此,性能往往也好于同步共享内存。其次,我们还可以通过设定约束(confinement)的方式将变量保护起来,来看以下代码:

代码语言:txt复制
data := make([]int, 4)  // need protection

loopData := func(handleData chan<- int) {
    for i := range data {
        handleData <- data[i]
    }
        close(handleData)
}

handleData := make(chan int)  // need protection
go loopData(handleData)

for num := range handleData {
    fmt.Println(num)
}

上面这个程序片段中,我们对切片 data 进行了多进程操作,并将计算结果发往通道 handleData,尽管代码这么写没有错,但它埋下了隐患。更好的做法是通过词法约束(lexical confinement),将切片和通道保护起来,从一开始避免读写的可能:

代码语言:txt复制
dataAndChanOwner := func() <-chan int {
        data := make([]int, 4)
        ch := make(chan int)
    go func() {
                for i := range data {
                ch <- data[i]
            }
                close(ch)
        }()
        return ch
}

// consumer
ch := dataAndChanOwner()
for num := range ch {
    fmt.Println(num)
}

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞