Scott

golang并发之秦皇寻找长生药 2 years ago

go
并发
5567个字符
共有462人围观

晴天霹雳

经过6代秦王的努力,到秦始皇即位的时候,秦国国力达到空前,13岁的他为了完成父辈、祖辈们的遗愿,励精图治,奋发图强,终于在39岁的时候一扫六合,统一天下。

此时的秦皇功勋卓著,威震海内,可谓风光无限,然而他却感觉到身体的不适. 查阅古籍《易经》占卜得知,自己最多只能活10年.

秦皇顿时慌了,他还有很多大事没做,为了给身体续命,秦皇把各大部门CEO招回咸阳,开了个会议:

会议的主题是: 搜索全国各大药店,不计一切代价找到下图所示的长生药.

与会主要CEO名单:

  • 蒙恬
  • 李斯
  • 赵高
  • 扶苏
  • 胡亥
  • 吕不韦
  • 徐福
  • 章邯
  • 王翦

秦皇寻找长生药1.0 - 要求所有人完成任务

接到命令的众人,带着人马陆续出发了.

9大人马,向全国各地奔赴,长城,沙漠,蓬莱…

丸子也接到了秦皇的召见,当场给秦皇做了2种方案

1.WaitGroup

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	var queens = []string{
		"蒙恬",
        "李斯",
        "赵高",
        "扶苏",
		"胡亥",
		"吕不韦",
        "徐福",		
        "章邯",
		"王翦",
		
	}

	for i := 0; i < len(queens); i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			doTask(queens[n])
		}(i)
	}

	wg.Wait()
}

func doTask(name string) {
	fmt.Printf("[%s]:出发了\n", name)
	rand.Seed(time.Now().Unix())
	for {
		//模拟找长生药的过程
		time.Sleep(time.Millisecond * 200)
		n := rand.Intn(10)
		if n == 8 {
			fmt.Printf("[%s]:完成了任务\n", name)
			break
		}
	}
}

模拟运行2次:

2.channel

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	allCompleted()
}

func allCompleted() {
	ch := make(chan string)
	var queens = []string{
		"蒙恬",
        "李斯",
        "赵高",
        "扶苏",
		"胡亥",
		"吕不韦",
        "徐福",		
        "章邯",
		"王翦",
	}
	for i := 0; i < len(queens); i++ {
		go func(n int) {
			res := doTask(queens[n])
			ch <- res
		}(i)
	}

	for i := 0; i < len(queens); i++ {
		fmt.Println(<-ch)
	}
}

func doTask(name string) string {
	fmt.Printf("[%s]:出发了\n", name)
	rand.Seed(time.Now().Unix())
	for {
		//模拟找长生药的过程
		time.Sleep(time.Millisecond * 200)
		n := rand.Intn(10)
		if n == 8 {
			return fmt.Sprintf("[%s]:完成了任务", name)
		}
	}
}

模拟运行2次:

秦皇寻找长生药2.0 - 任意人完成任务即可

经过了一段时间的搜寻,秦皇发现,要求所有人都完成任务不仅消耗财力,而且效率低下,事实是 - 只要有一对人马完成任务即可,因此寻找长生药升级成了2.0

buffered channel

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	result := onlyOneCompleted()
	fmt.Println(result)
}

func onlyOneCompleted() string {
	ch := make(chan string)
	var queens = []string{
		"蒙恬",
		"李斯",
		"赵高",
		"扶苏",
		"胡亥",
		"吕不韦",
		"徐福",
		"章邯",
		"王翦",
	}
	for i := 0; i < len(queens); i++ {
		go func(n int) {
			res := doTask(queens[n])
			ch <- res
		}(i)
	}

	return <-ch
}

func doTask(name string) string {
	fmt.Printf("[%s]:出发了\n", name)
	rand.Seed(time.Now().Unix())
	for {
		//模拟找长生药的过程
		time.Sleep(time.Millisecond * 200)
		n := rand.Intn(10)
		if n == 8 {
			return fmt.Sprintf("[%s]:完成了任务", name)
		}
	}
}

模拟运行2次:

看着结果 好像是没问题,其实上面的代码存在安全隐患,我们知道:channel的2端不论是发送方还是接收方只要一方不在线 另一方就会阻塞

func main() {
	fmt.Println("before:", runtime.NumGoroutine())
	result := onlyOneCompleted()
	fmt.Println(result)
	time.Sleep(time.Second * 10)
	fmt.Println("after:", runtime.NumGoroutine())
}

模拟运行2次:

可以看到除了main goroutine外,还有8个被阻塞的goroutine,这种“代码缺陷”在当前示例中是没有任何影响的

但如果是服务器中或者后台程序,那么goroutine成倍的增长势必会在某一时刻将服务器的内存撑爆,而导致内存泄漏(memory leak)

所以上述代码还需要改进,改进也很简单,就是不让goroutine阻塞即可,buffered channel就可以做到.

func onlyOneCompleted() string {
	var queens = []string{
		"蒙恬",
		"李斯",
		"赵高",
		"扶苏",
		"胡亥",
		"吕不韦",
		"徐福",
		"章邯",
		"王翦",
	}

	ch := make(chan string, len(queens))
	for i := 0; i < len(queens); i++ {
		go func(n int) {
			res := doTask(queens[n])
			ch <- res
		}(i)
	}

	return <-ch
}

改完代码之后,我们再来测试一下:

秦皇寻找长生药3.0 - 任意人完成任务即可

2.0虽然可以完成任务,但是资源却存在极大的浪费,其他8个人可没闲着,一直在“内耗”呢!

所以需要通知其他所有人 - “事已经办妥了,大家都休息吧”

close(channel)就可以达到广播的效果

我们来改造一下代码

func onlyOneCompleted() string {
	var queens = []string{
		"蒙恬",
		"李斯",
		"赵高",
		"扶苏",
		"胡亥",
		"吕不韦",
		"徐福",
		"章邯",
		"王翦",
	}

	ch := make(chan string)
	for i := 0; i < len(queens); i++ {
		go func(n int) {
			res := doTask(queens[n])
			ch <- res
		}(i)
	}

	result := <-ch
	close(ch)
	return result
}

运行后,发现报错了,原因是向已经关闭的channel中发送数据

切记: channel的close是在发送方

既然这种方案行不通, 我们来试试context

context

context比较强大,

在Golang中,Context是用于控制协程退出的工具。Context可以让我们在多个goroutine之间传递取消信号,并且能够及时终止协程的执行。它非常适用于一些需要及时停止的场景,比如网络请求、任务执行等。

使用Context的基本流程如下:

  • 1.使用context包创建一个Context对象。
  • 2.在需要控制的goroutine中,调用Context的Done方法来检查是否需要退出。
  • 3.如果需要退出,则退出当前goroutine。
package main

import (
	"context"
	"fmt"
	"math/rand"
	"runtime"
	"time"
)

func main() {
	fmt.Println("before:", runtime.NumGoroutine())
	onlyOneCompleted()
	// time.Sleep(time.Second * 10)
	for {
		time.Sleep(time.Second * 1)
		fmt.Println("after:", runtime.NumGoroutine())
	}

}

func onlyOneCompleted() {
	done := make(chan string)
	ctx, cancel := context.WithCancel(context.Background())
	var queens = []string{
		"蒙恬",
		"李斯",
		"赵高",
		"扶苏",
		"胡亥",
		"吕不韦",
		"徐福",
		"章邯",
		"王翦",
	}
	for i := 0; i < len(queens); i++ {
		go func(n int, ctx context.Context) {
			fmt.Printf("[%s]:出发了\n", queens[n])

			for {
				rand.Seed(time.Now().Unix())
				//模拟找长生药的过程
				time.Sleep(time.Millisecond * 20)
				x := rand.Intn(3)
				if x == 1 {
					//必须在最后一步,即即将判定任务完成那里去check一下是否有 <-ctx.Done()的信号
					//否则总是会有那么几个携程被done阻塞 一直无法退出 导致内存泄漏
					if isCancelled(ctx) {
						fmt.Printf("%s was cancelled\n", queens[n])
						return
					}
					done <- fmt.Sprintf("[%s]:完成了任务", queens[n])
					// fmt.Printf("[%s]:完成了任务\n", queens[n])
					// cancel()
					return
				}
			}

		}(i, ctx)
	}
	fmt.Println(<-done)
	cancel()
}

func isCancelled(ctx context.Context) bool {
	select {
	case <-ctx.Done():
		return true
	default:
		return false
	}
}

查看输出 是我们想要的结果