详解Golang并发控制的三种方案

来自:网络
时间:2024-08-28
阅读:

Channel

Channel是Go在语言层面提供的一种协程间的通信方式,我们可以通过在协程中向管道写入数据和在待等待的协程中读取对应协程的次数来实现并发控制。

func main() {
    intChan := make(chan int, 5)
    waitCount := 5
    for i := 0; i < waitCount; i++ {
       go func() {
          intChan <- 1
       }()
    }

    for i := 0; i < waitCount; i++ {
       <-intChan

    }
    fmt.Println("主进程结束")
}

WaitGroup

waitgroup通常应用于等待一组“工作协程”结束的场景,waitgroup底层是由一个长度为3的数组实现的,其内部有两个计数器,一个是工作协程计数器、一个是坐等协程计数器,还有一个是信号量。工作协程全部运行结束后,工作协程计数器将置为0,会释放对应坐等协程次数的信号量。

两点注意:

  • Add()方法中的参数大小要于工作协程的数量相等,否则会导致坐等协程一直等待,触发死锁panic

  • Done()方法执行的次数要与Add()方法中的工作协程计数器的数量一致,否则当工作协程计数器<0时,会触发panic【panic: sync: negative WaitGroup counter】

func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
       time.Sleep(3 * time.Second)
       fmt.Println("等待三分钟的协程结束了")
       wg.Done()
    }()

    go func() {
       time.Sleep(3 * time.Second)
       fmt.Println("等待三分钟的协程结束了")
       wg.Done()
    }()

    wg.Wait()
}

Context

适用于一个协程派生出多个协程的情况,可以控制多级的goroutine。我们可以通过一个Context对象,对派生出来的树状goroutine进行统一管理,并且每个goroutine具有相同的上下文。做统一关闭操作、统一定时关闭、统一传值的操作。多个上下文协程之间可以互相嵌套配合。

详解Golang并发控制的三种方案

golang实现了四种原生的上下文对象

  • emptyCtx: 该上下文对象一般是作为父节点的,如果没有父节点,我们通常使用context.Background()方法来获取emptyCtx对象,并将其作为创建其他节点的父节点。

  • cancelCtx: 该上下文对象可以关闭所有拥有同一个上下文的goroutine,通过在子协程中监听cancelCtx.Done方法,来结束所有的派生协程。具体代码看下方,我们通过WithCancel()方法来获取该对象。

  • timerCtx:该上下文对象是对cancelCtx对象的进一步封装,比cancelCtx主动关闭之外,多了了一个定时关闭功能。我们可以通过WithTimeout()和WithDeadline()这两种方法来获取该对象。其中WithTimeout()和WithDeadline()这两种方法点是WithTimeout()是设置过一段时间关闭上下文,WithDeadline()是设置那一个时间点来关闭这一个上下文。

  • valueCtx:该上下文对象并不用于进行协程的控制,而是在多级协程之间进行值得传递,方便共享一些相同得上下文内容。

以上除emptyCtx外的上下文对象和获取实例的方法如下图所示:

详解Golang并发控制的三种方案

 Context示例代码

cancelCtx

我们在所有的派生协程中传入相同的cancelContext对象,并在每一个子协程中使用switch-case结构监听上下文对象是否关闭,如果上下文对象关闭了,ctx.Done()返回的管道就可以读取到一个元素,使所在的case语句可执行,之后退出switch结构,执行协程中的其他代码。

func main() {
	ctx, cancelFunc := context.WithCancel(context.Background())
	deadline, ok := ctx.Deadline()
	fmt.Println(deadline, ok)
	done := ctx.Done()
	fmt.Println(reflect.TypeOf(done))
	fmt.Println(done)

	go HandelRequest(ctx)
	//<-done 阻塞当前一层的goroutine
	time.Sleep(5 * time.Second)
	fmt.Println("all goroutines is stopping!")
	cancelFunc()
	err := ctx.Err()
	fmt.Println(err) //context canceled
	time.Sleep(5 * time.Second)
}

func HandelRequest(ctx context.Context) {
	go WriteMysql(ctx)
	go WriteRedis(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done")
			return
		default:
			fmt.Println("等一等,Handler正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("等一等,Redis正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteMysql(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteMysql Done.")
			return
		default:
			fmt.Println("等一等,Mysql正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

timerCtx

这里代码以WithTimeout举例,相比与我们之前的手动调用关闭,使用timerCtx定时上下文对象后,可以是实现到达指定的时间自动进行关闭的操作。

func main() {
	deadline, _ := context.WithTimeout(context.Background(), 5*time.Second)
	go HandelRequest(deadline)

	time.Sleep(10 * time.Second)

}

func HandelRequest(ctx context.Context) {
	go WriteMysql(ctx)
	go WriteRedis(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done")
			return
		default:
			fmt.Println("等一等,Handler正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("等一等,Redis正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteMysql(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteMysql Done.")
			return
		default:
			fmt.Println("等一等,Mysql正在执行中")
			time.Sleep(2 * time.Second)
		}
	}
}

valueCtx

我们可以通过嵌套WithValue上下文,来进行多个key-value在派生协程中传递,共享常量

func main() {
	ctx, cancelFunc := context.WithCancel(context.Background())
	value1 := context.WithValue(ctx, "param1", 1)
	value2 := context.WithValue(value1, "param2", 2)
	go ReadContextValue(value2)

	time.Sleep(10 * time.Second)
	cancelFunc()
	time.Sleep(5 * time.Second)
}

func ReadContextValue(ctx context.Context) {
	fmt.Println(ctx.Value("param1"))
	fmt.Println(ctx.Value("param2"))
}
返回顶部
顶部