今天开始写Golang之Golang并发编程

221 阅读5分钟

src=http___nimg.ws.126.net__url=http%3A%2F%2Fdingyue.ws.126.net%2F2022%2F0616%2F33d6b7f6j00rdj8ch000dc000hs00a0g.jpg&thumbnail=660x2147483647&quality=80&type=jpg&refer=http___nimg.ws.126.webp

本文对Golang并发编程进行讲解,源于自己的学习体验。文笔一般,才疏学浅,请多指教!!

Go并发编程

Go协程(Goroutine

它常常被用于进行多任务,即并发作业 ,Go 的协程却依赖于线程来进行

线程和内核密不可分,协程是在应用层

GMP

在Go中主要是使用GMP调度模型进行调度的。

GMP模型其实指的是:G:Go协程、M:线程、P、逻辑处理器。

它们之间的关系,借用一张图来开是这样的:

本图来源于本站其他文章

它的逻辑为:

G:

  1. 当一个G创建时,会优先放入P的runnext
  2. 当runnext放满时,会把当前的G放入本地队列。
  3. 当本地队列也放满时,会把当前的本地队列的一半和当前的G一起放入全局队列。

M:

  1. 先从P的本地队列(优先runnext)中取出执行。
  2. 定期处理全局队列的G,一般每61次一处理,将全局队列的G平均分配给每个P。
  3. 当当前的P没有G了,会随机挑一个P,拿一半的G执行。

P的结构特点:

  1. 长度锁定256,但是Go有无限长度的全局队列。
  2. 本地队列是数组,全局队列是链表。

chan 通道

通道是Golang并发的基础,通道可以使得Go协程之间的数据,状态进行共享。

创建一个通道ch:=make(chan int) 创建了一个int类型的通道

通道分为缓存通道和非缓存通道,它们的区别在于是否定义长度

a := make(chan int)
b := make(chan int, 10)

如何往通道存放数据呢?

// 创建通道
ch := make(chan int) 
a:=1

ch<-a //将a放入通道

b:=<-ch //取出通道的数值放入b

例子1: Go协程的基本实现

work1 和 work2 是两个机器人,它们需要找到数组中的2,我们希望它们比一比谁最快找到。


func work1(arr []int) {
   for i := 0; i < len(arr); i++ {
      if arr[i] == 2 {
         fmt.Println("work1 get the number")
         return
      } else {
         fmt.Println("work1 mine")
      }
   }
}

func work2(arr []int) {
   for i := 0; i < len(arr); i++ {
      if arr[i] == 2 {
         fmt.Println("work2 get the number")
         return
      } else {
         fmt.Println("work2 mine")
      }
   }
}

func main() {
   arr := []int{1, 3, 5, 6, 7, 1, 3, 5, 7, 2, 3, 4}
   go work1(arr)
   go work2(arr)
   time.Sleep(1 * time.Second)
}

希望你能亲手实现这个例子。

当你运行多次你会发现,它们的顺序是不同的,但是会出现两者都get的情况。这样是比较浪费时间的,如何改善这种情况呢?

采用通道:

func work1(arr []int, ch chan bool) {
   for i := 0; i < len(arr); i++ {
      select {
      case <-ch:
         fmt.Println("work1 say 'GG' ")
         return
      default:
         if arr[i] == 2 {
            fmt.Println("work1 get the number")
            ch <- true
            return
         } else {
            fmt.Println("work1 mine")
         }
      }
   }
}

func work2(arr []int, ch chan bool) {
   for i := 0; i < len(arr); i++ {
      select {
      case <-ch:
         fmt.Println("work2 say 'GG'")
         return
      default:
         if arr[i] == 2 {
            fmt.Println("work2 get the number")
            ch <- true
            return
         } else {
            fmt.Println("work2 mine")
         }
      }
   }
}

func main() {
   arr := []int{1, 3, 5, 6, 7, 1, 3, 5, 7, 2, 3, 4}
   ch := make(chan bool)

   go work1(arr, ch)
   go work2(arr, ch)
   time.Sleep(1 * time.Second)
}

这里使用的Select是一个特殊的用法,比较类似Swtichselect 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认(default)的子句应该总是可运行的。

同步协程的方法

根据上面的介绍我们已经得知了第一种同步的方法:通道chan

下面我们介绍第二种方法:WaitGroup

sync.WaitGroup

这是Go提供的等待组方法,通过使用WaitGroup可以有效的同步协程。


func main() {
   var wg sync.WaitGroup
   wg.Add(2)

   go func() {
      time.Sleep(1000)
      fmt.Println("1👌")
      wg.Done()
   }()

   go func() {
      time.Sleep(500)
      fmt.Println("2👌")
      wg.Done()
   }()
   wg.Wait()
   fmt.Println("ALL Finish")

}

context

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

创建context:

context.Backgroundcontext.Todo可以创建一个context

它有多个方法:

  • WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context
  • WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消
  • WithTimeoutWithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
  • WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到

WithCancel

func main() {
   ctx, cancel := context.WithCancel(context.Background()) 
   go watch(ctx, "【监控1】")
   go watch(ctx, "【监控2】")
   go watch(ctx, "【监控3】")
   time.Sleep(2 * time.Second)
   fmt.Println("可以了,通知监控停止")
   cancel()
   //为了检测监控过是否停止,如果没有监控输出,就表示停止了
   time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Println("监控停止")
         return
      default:
         fmt.Println(name + " 监控中...")
      }

   }
}

WithDeadline

func main() {
   ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3)) //3秒

   go watch(ctx, "监控1")
   go watch(ctx, "监控2")

   fmt.Println("现在开始等待5秒,time=", time.Now().Unix())
   time.Sleep(5 * time.Second)

   fmt.Println("等待5秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
   cancel()

}

func watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Println("监控停止,time=", time.Now().Unix())
         return
      default:
         fmt.Println(name + " 监控中...")
      }

   }
}

WithTimeout

WithTimeoutWithCancel的区别在于参数的不同,WithTimeout传入的是时间长短,而WithCancel是相对时间。

func main() {
   ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) //3秒
   fmt.Println("开始监控,time=", time.Now().Unix())
   go watch(ctx, "监控1")
   go watch(ctx, "监控2")

   fmt.Println("现在开始等待5秒,time=", time.Now().Unix())
   time.Sleep(5 * time.Second)

   fmt.Println("等待5秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
   cancel()

}

func watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Println("监控停止,time=", time.Now().Unix())
         return
      default:
         fmt.Println(name + " 监控中...")
      }

   }
}

sync.Mutex 互斥锁

引用自:tour.go-zh.org/concurrency…

sync.Mutex保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutualexclusion)* ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock
type SafeCounter struct {
   v   map[string]int
   mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
   c.mux.Lock()
   // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
   c.v[key]++
   c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
   c.mux.Lock()
   // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
   defer c.mux.Unlock()
   return c.v[key]
}

func main() {
   c := SafeCounter{v: make(map[string]int)}
   for i := 0; i < 1000; i++ {
      go c.Inc("some-key")
   }

   time.Sleep(time.Second)
   fmt.Println(c.Value("some-key"))
}

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿