Golang中的并发控制与通信详解

        开发go程序的时候,时常需要使用goroutine并发处理任务,有时候这些goroutine是相互独立的,而有的时候,多个goroutine之间常常是需要同步与通信的。那么Golang中控制并发的方法,主要有哪几类呢?大致可以分为三类:

  • 全局共享变量
  • channel
  • context

1. 全局共享变量

  • 应用场景
    • 主goroutine中通知子goroutine停止运行
  • 代码比较简单,直接上代码
package main

import (
	"fmt"
	"time"
)

func main() {
	running := true
	f := func() {
		for running {
			fmt.Println("sub proc running...")
			time.Sleep(1 * time.Second)
		}
		fmt.Println("sub proc exit")
	}
	go f()
	go f()
	go f()

	time.Sleep(2 * time.Second) // sleep 2秒,让子goroutine运行一会
	running = false // running置为false,告诉子gorroutine该退出了

	time.Sleep(3 * time.Second)
	fmt.Println("main proc exit")
}
  • 分析
    • 优势

      • 全局变量的优势是简单方便,不需要过多繁杂的操作,通过一个变量就可以控制所有子goroutine的开始和结束;

    • 缺点
      • 功能有限,由于架构所致,该全局变量只能是多读一写,否则会出现数据同步问题,当然也可以通过给全局变量加锁来解决这个问题,但那就增加了复杂度
      • 不适合用于子goroutine间的通信,因为全局变量可以传递的信息很小;
      • 还有就是主进程无法等待所有子goroutine退出,因为这种方式只能是单向通知
  • 结论
    • 一般来说,尽量不要使用全局变量来作为多goroutine之间的协作通信,传递的信息有限,且如果涉及到多个goroutine同时写的情况,增加实现难度和代码复杂度。当然,有一些场景,不得不用全局变量,也不用害怕,大胆使用,做好访问控制,例如如下场景:比如我要写一个爬虫程序,需要一个全局map来保存哪些url已经被访问过了,当然这个map是需要加锁保护的。

2. channel

        对于1中的场景:主goroutine中通知子goroutine停止运行,使用channel是一种更优雅的实现方式。代码也比较简单,代码如下:

package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

func consumer(stop <-chan bool, i int) {
	for {
		select {
		case msg, ok := <-stop:
			if !ok {
				fmt.Println("channel stopped, exit sub goroutine, msg: ", msg)
			}
			return
		default:
			fmt.Println(i, " running...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	stop := make(chan bool)
	var wg sync.WaitGroup
	// Spawn example consumers
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(stop <-chan bool, i int) {
			defer wg.Done()
			consumer(stop, i)
		}(stop, i)
	}
	waitForSignal()
	close(stop)
	fmt.Println("stopping all jobs!")
	wg.Wait()
}

func waitForSignal() {
	sigs := make(chan os.Signal)
	signal.Notify(sigs, os.Interrupt)
	signal.Notify(sigs, syscall.SIGTERM)
	<-sigs
}

3. context

        Context的创建和调用关系是层层递进的,也就是我们通常所说的链式调用,类似数据结构里的树,从根节点开始,每一次调用就衍生一个叶子节点。首先,生成根节点,使用context.Background方法生成,而后可以进行链式调用使用context包里的各类方法,context包里的所有方法:

  • func Background() Context
  • func TODO() Context
  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  • func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
  • func WithValue(parent Context, key, val interface{}) Context

         想要了解更多有关context的东西,可以直接去看源码,这里有有一篇很不错的源码剖析帖:context源码剖析,这里仅以WithCancel和WithValue方法为例来实现控制并发和通信,代码如下:

package main

import (
	"context"
	"crypto/md5"
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
	"time"
)

type favContextKey string

func main() {
	wg := &sync.WaitGroup{}
	values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
	ctx, cancel := context.WithCancel(context.Background())

	for _, url := range values {
		wg.Add(1)
		subCtx := context.WithValue(ctx, favContextKey("url"), url)
		go reqURL(subCtx, wg)
	}

	go func() {
		time.Sleep(time.Second * 3)
		cancel()
	}()

	wg.Wait()
	fmt.Println("exit main goroutine")
}

func reqURL(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	url, _ := ctx.Value(favContextKey("url")).(string)
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("stop getting url:%s\n", url)
			return
		default:
			r, err := http.Get(url)
			if r.StatusCode == http.StatusOK && err == nil {
				body, _ := ioutil.ReadAll(r.Body)
				subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
				wg.Add(1)
				go showResp(subCtx, wg)
			}
			r.Body.Close()
			//启动子goroutine是为了不阻塞当前goroutine,这里在实际场景中可以去执行其他逻辑,这里为了方便直接sleep一秒
			// doSometing()
			time.Sleep(time.Second * 1)
		}
	}
}

func showResp(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println("stop showing resp")
			return
		default:
			//子goroutine里一般会处理一些IO任务,如读写数据库或者rpc调用,这里为了方便直接把数据打印
			fmt.Println("printing ", ctx.Value(favContextKey("resp")))
			time.Sleep(time.Second * 1)
		}
	}
}

 

参考资料:

https://www.jianshu.com/p/6e40d6a5db52

https://studygolang.com/articles/9532

https://leileiluoluo.com/posts/golang-channels.html

https://www.jianshu.com/p/0514c4ba464c

你可能感兴趣的:(golang)