go语言并发

1. Go Mutex

Go语言中goroutines共享内存。这对性能有好处,但是从多个goroutine修改相同的内存是不安全的, 会导致数据争用和崩溃。这是Go的座右铭:不要通过共享内存进行通信;而是通过通信共享内存。
确保goroutine独占访问数据的另一种方法是使用互斥体。

1.1 sync.Mutex

var cache map[int]int
var mu sync.Mutex

func expensiveOperation(n int) int {
    // in real code this operation would be very expensive
    return n * n
}

func getCached(n int) int {
    mu.Lock()
    v, isCached := cache[n]
    mu.Unlock()
    if isCached {
        return v
    }

    v = expensiveOperation(n)

    mu.Lock()
    cache[n] = v
    mu.Unlock()
    return v
}

func accessCache() {
    total := 0
    for i := 0; i < 5; i++ {
        n := getCached(i)
        total += n
    }
    fmt.Printf("total: %d\n", total)
}

cache = make(map[int]int)
go accessCache()
accessCache()

sync.Mutex的初始值是有效的mutex,所以不必初始化。为了提高性能,我们希望最小化持有锁的时间。与许多其他语言不同,Go互斥锁是非递归的。如果相同的goroutine尝试两次对互斥量进行Lock(),则第二个Lock()将永远阻塞。

1.2 sync.RWMutex

在sync.Mutex Lock()中始终采用排他锁。
在重读场景中,如果我们允许多个读者但只允许一个写者,则可以提高性能。sync.RWMutex具有两种锁定功能:用于读取的锁定和用于写入的锁定。它遵循以下规则:

  • 写锁采用排他锁
  • 读锁将允许其他读但不允许写

1.3 Mutex 陷阱

不要拷贝互斥锁。sync.Mutex变量的副本以与原始互斥锁相同的状态开始,但不是同一个Mutex。
拷贝互斥锁几乎总是错误的, 比如通过将其传递给另一个函数或将其嵌入结构中并复制该结构。如果要共享互斥变量,请将其作为指针* sync.Mutex传递。

互斥体不是递归的(又名可重入)。在某些语言中,互斥锁是递归的,即同一线程可以多次锁定同一互斥锁。
在Go中sync.Mutex是非递归的。在同一goroutine中两次调用Lock将导致死锁。

1.4 检测争夺

如果不使用sync.Mutex来确保goroutine之间的数据独占访问,或者忘记锁定程序的某些部分,则会引起数据争夺。数据争用可能导致内存损坏或崩溃。使用Go可以很容易地通过附加检查来对代码进行检测。
使用-race进行go build或go run。 例如:go run -race data_race.go

2. Goroutine

go语言本身内置了调度和上下文的切换,开发者只需要关注自己的函数,并且交给goroutine就可以了

2.1 waitGroup

多个Goroutine

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // 结束后 goroutine的注册数量就会-1
    fmt.Println("Hello Goroutine!", i)
}

func main() {
    for i := 0; i < 2; i++ {
        wg.Add(1) // 注册goroutine
        go hello(i) // 启动goroutine
    }
    wg.Wait() // 等待所有的goroutine完成
}

多个goroutine并发执行,并不能保证顺序,同时main函数结束,协程也就会结束,所以想保障正确的逻辑,就需要通过waitGroup来完成。

  • wg。Add(): main协程通过此方法完成wg的注册,并将数量+1
  • wg。Done():work协程通过此方法完成任务,并将数量-1
  • wg。Wait():main协程会一直阻塞,直到所有的work协程完成

2.2 Channels

java和go中sync包通过共享内存实现通信,go channel提倡通过通信共享内存,这就是典型的CSP思想的实践。
程序在给channel分配内存时,可以指定channel的容量大小。可以根据是否具有容量将channel分为无缓冲和缓冲两类。

// 同步
c1 := make(chan int)

// 异步
c2 := make(chan int,2)
# 代码
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

2.3 池化

Goroutine是很轻量级的协程,但是频繁的创建协程也需要很大的开销,主要表现在创建(内存),调度(调度器),删除(GC)。
虽然Goroutine是用户态上的操作,但是最终都需要交给系统线程,而系统线程也有承载压力,所以我们需要协程池来降低这部分的压力。
协程池通过维护一组协程(也就是Goroutine),来处理并发任务。协程池可以有效地控制协程的数量,避免过多的协程导致系统资源的浪费,从而提高程序的性能和稳定性。
实现协程池一般需要以下几个步骤:

  • 创建协程池对象。协程池对象中需要包含协程池大小、任务队列、信号量等信息。
  • 初始化协程池。在初始化协程池时,需要创建一定数量的协程(Goroutine)并加入到协程池中。
  • 向协程池中提交任务。提交任务时,将任务加入到任务队列中,并通过信号量激活一个空闲的协程来执行任务。
  • 执行任务。协程从任务队列中取出任务并执行,当任务队列为空时,协程将进入等待状态。
  • 停止协程池。停止协程池时,需要将所有任务执行完毕,并关闭所有协程。

2.4 GMP模型
go语言并发_第1张图片

结构图如下:
go语言并发_第2张图片
go func()执行过程
go语言并发_第3张图片

你可能感兴趣的:(编程语言,golang,算法,服务器)