Go与并发编程实战

暗夜行者 2021-09-01 ⋅ 12 阅读

并发编程是现代计算机系统设计中非常重要的一部分。随着处理器核心数量的不断增加,充分利用多核并发能力已成为一项不可或缺的要求。Go语言作为一个现代化、开源的编程语言,天生具备了良好的并发编程能力。本博客将介绍Go语言并发编程的实战经验和一些常见的并发模式。

Goroutine和Channel

Go语言中的Goroutine是轻量级线程,可以在程序中同时执行多个任务。Goroutine的特点是创建和销毁都很快,可以快速地响应任务的调度。与传统的线程相比,Goroutine的成本非常低,因此可以同时创建大量的Goroutine来处理并发任务,提高程序的吞吐量。

Goroutine之间的通信通过Channel来完成。Channel是一种类型安全的消息队列,用于在Goroutine之间传递数据。Channel既可以用于同步Goroutine的执行,也可以用于在Goroutine之间传递结果或消息。

下面是一个使用Goroutine和Channel实现并发计算的示例代码:

package main

import "fmt"

func calculateSum(numbers []int, resultChan chan int) {
  sum := 0
  for _, number := range numbers {
    sum += number
  }
  resultChan <- sum
}

func main() {
  numbers := []int{1, 2, 3, 4, 5}

  resultChan := make(chan int)

  go calculateSum(numbers[:len(numbers)/2], resultChan)
  go calculateSum(numbers[len(numbers)/2:], resultChan)

  sum1 := <-resultChan
  sum2 := <-resultChan

  totalSum := sum1 + sum2
  fmt.Println("Total sum:", totalSum)
}

上述代码中,我们使用两个Goroutine并发地计算numbers切片的和,并通过resultChan将计算结果传递回主Goroutine。最后,我们将两个部分和相加得到总和并打印输出。

并发安全

在并发编程中,由于多个Goroutine可能同时访问共享的资源,容易出现竞态条件(Race Condition)和其他并发问题。为了确保程序的正确性,我们需要采取措施保证并发安全。

下面是一些在Go语言中确保并发安全的常见技术:

互斥锁

互斥锁(Mutex)是一种简单而有效的并发控制机制。在访问共享资源之前,先获取互斥锁,操作完成后释放互斥锁。通过互斥锁,我们可以确保同一时间只有一个Goroutine能够访问共享资源,避免竞态条件的发生。

package main

import (
  "fmt"
  "sync"
)

var (
  sum int
  mutex sync.Mutex
)

func addToSum(number int) {
  mutex.Lock()
  sum += number
  mutex.Unlock()
}

func main() {
  var wg sync.WaitGroup

  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      addToSum(i)
    }(i)
  }

  wg.Wait()

  fmt.Println("Sum:", sum)
}

在上述代码中,我们使用互斥锁确保对sum变量的并发访问安全。在addToSum函数中,我们在访问sum变量之前获取互斥锁,操作完成后释放互斥锁。

原子操作

Go语言提供了一些原子操作函数,用于执行不可分割的操作。原子操作可以确保在并发环境下的操作是原子的,不会被其他Goroutine中断。通过原子操作,我们可以避免竞态条件和死锁等并发问题。

package main

import (
  "fmt"
  "sync/atomic"
)

var sum int64

func addToSum(number int64) {
  atomic.AddInt64(&sum, number)
}

func main() {
  var wg sync.WaitGroup

  for i := int64(0); i < 1000; i++ {
    wg.Add(1)
    go func(i int64) {
      defer wg.Done()
      addToSum(i)
    }(i)
  }

  wg.Wait()

  fmt.Println("Sum:", sum)
}

在上述代码中,我们使用原子操作函数atomic.AddInt64确保对sum变量的并发访问安全。atomic.AddInt64函数会原子地将传入的值加到sum变量上。

并发模式

在实际的并发编程中,我们可以使用一些常见的并发模式来提高程序的性能和可伸缩性。下面是一些常见的并发模式:

Worker Pool

Worker Pool模式是一种常见的并发模式,适用于处理大量独立的任务。在Worker Pool模式中,我们使用一组固定数量的Goroutine(Worker)来处理任务(Job)。任务通过Channel传递给Worker,并返回处理结果。

package main

import (
  "fmt"
  "sync"
)

type Job struct {
  id int
}

type Result struct {
  job Job
  sum int
}

func worker(id int, jobs <-chan Job, results chan<- Result) {
  for job := range jobs {
    sum := 0
    for i := 0; i < job.id; i++ {
      sum += i
    }
    result := Result{job, sum}
    results <- result
  }
}

func main() {
  var wg sync.WaitGroup

  numWorkers := 5
  numJobs := 100

  jobs := make(chan Job, numJobs)
  results := make(chan Result, numJobs)

  for i := 0; i < numWorkers; i++ {
    wg.Add(1)
    go func(id int) {
      defer wg.Done()
      worker(id, jobs, results)
    }(i)
  }

  for i := 0; i < numJobs; i++ {
    jobs <- Job{id: i}
  }

  close(jobs)

  wg.Wait()

  for result := range results {
    fmt.Println("Job", result.job.id, "Sum:", result.sum)
  }
  
  close(results)
}

在上述代码中,我们使用Worker Pool模式处理一组独立的任务。首先创建了一定数量的Workers(这里是5个),然后创建了两个Channel:用于传递任务的jobs和用于传递结果的results。通过jobs Channel将所有任务发送给Workers,并通过results Channel接收任务结果。

Future

Future模式是一种常见的并发模式,用于异步执行计算任务并在将来获取计算结果。在Future模式中,我们将任务封装为一个Future对象,对象内部执行计算,并提供一个方法来获取计算结果。在调用该方法时,如果计算还未完成,可以等待计算结果返回。

package main

import (
  "fmt"
  "sync"
  "time"
)

type Future struct {
  value int
  ready bool
  mutex sync.Mutex
  cond *sync.Cond
}

func (f *Future) Set(value int) {
  f.mutex.Lock()
  defer f.mutex.Unlock()

  f.value = value
  f.ready = true

  f.cond.Broadcast()
}

func (f *Future) Get() int {
  f.mutex.Lock()
  defer f.mutex.Unlock()

  for !f.ready {
    f.cond.Wait()
  }

  return f.value
}

func calculateSum(numbers []int) *Future {
  future := &Future{
    ready: false,
    cond: sync.NewCond(&sync.Mutex{}),
  }
  
  go func() {
    sum := 0
    for _, number := range numbers {
      sum += number
    }

    time.Sleep(2 * time.Second) // 模拟一个耗时计算

    future.Set(sum)
  }()

  return future
}

func main() {
  numbers := []int{1, 2, 3, 4, 5}
  future := calculateSum(numbers)

  result := future.Get()

  fmt.Println("Sum:", result)
}

在上述代码中,我们使用Future模式异步计算numbers切片的和。calculateSum函数返回一个Future对象,内部启动一个Goroutine来完成计算,并通过Set方法将结果设置到Future对象中。在需要计算结果时,调用Future对象的Get方法来等待结果返回。

总结

本博客介绍了Go语言并发编程的实战经验和一些常见的并发模式。通过使用Goroutine和Channel,我们可以实现高效的并发编程。同时,我们还讨论了如何确保并发安全,以及一些常见的并发模式,如Worker Pool和Future。通过合理的使用并发编程技术和模式,我们可以提高程序的性能和可伸缩性,更好地利用计算机系统的并发能力。


全部评论: 0

    我有话说: