go 并发编程 Goroutines(一)

文章目录前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结前言项目选择Go通常是因为它的并发特性。Go 团队已经花了很大的精力使 Go 中的并发性 变得廉价(就硬件资源而言)和高效,但是使用 Go 的并发特性来编写代码是可能的,这既不可靠也不可行。我想留给你一些建议,以避免Go的并发特性带来的一些陷阱提示:以下是本篇文章正文内容,下面案例可供参考一、让自己忙碌起来或者自己完成工作英文:Keep yourself busy or do.
摘要由CSDN通过智能技术生成

文章目录


前言

项目选择Go通常是因为它的并发特性。Go 团队已经花了很大的精力使 Go 中的并发性  变得廉价(就硬件资源而言)和高效,但是使用 Go 的并发特性来编写代码是可能的,这既不可靠也不可行。我想留给你一些建议,以避免Go的并发特性带来的一些陷阱


提示:以下是本篇文章正文内容,下面案例可供参考

一、让自己忙碌起来或者自己完成工作

英文:Keep yourself busy or do the work yourself

我们看一下第一个例子

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello")
	})

    //开启一个 协程 处理http的监听
	go func() {
		if err := http.ListenAndServe(":8080", nil); err != nil {
			log.Fatal(err) // 如果出现错误立即退出 
		}
	}()
    

    //for 死循环
	for {
	}
}

这是一个简单的http的服务器,for 循环 在无限的浪费 CPU的资源 

二、将并发 或者 (选择权)留给 调用者

1.例子 一


func debug(){

	go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func app(){
	
	go func() {
		mux := http.NewServeMux()
		mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
			fmt.Fprintln(resp, "Hello, QCon!")
		})
		http.ListenAndServe("0.0.0.0:8080", mux)
	}()
	
}

func main() {

	 debug()  // debug
	 app()    // app traffic

	select{}
}

看上面的这个例子

开起了两个监听服务,8080是线上访问,8001 的debug

问题1.线上出现事故想要调试结果 debug 不知道什么时候退出了。这个时候就傻x 了?

问题2.main 函数进行了 一个 select{} 是不是一直阻塞 跟个傻x 一样!

问题3.如果说这两个服务在你的其他包里,也不写注释,其他人会发现这是一个 协程吗?

   例子  二

func debug(){

	 http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func app(){

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	http.ListenAndServe("0.0.0.0:8080", mux)

}

func main() {

	 go debug()  // debug
	 go app()    // app traffic

	select{}
}

这个例子 遵循了 将并发留给调用者

正如 Go 中的函数将并发性留给调用方一样,应用程序应该将监视其状态的工作留给调用它们的程序,如果调用失败,应用程序应该重新启动它们。不要让您的应用程序自己负责重新启动,这是一个最好从应用程序外部处理的过程

问题1.线上出现事故想要调试结果 debug 不知道什么时候退出了。这个时候就傻x 了?

问题2.main 函数进行了 一个 select{} 是不是一直阻塞 会显得很尴尬

例子三

func debug(){

	 if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil{
		 log.Fatal(err)
	 }
}

func app(){

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
		log.Fatal(err)
	}

}

func main() {

	 go debug()  // debug
	 go app()    // app traffic

	select{}
}

这个例子 如果两个监听其他一个返回err 就立即终止了。

我们真正想要的是将发生的任何错误传递回 goroutine 的发起者,这样它就可以知道 goroutine 为什么停止,可以干净地关闭进程

存在的问题

1.log.fatal  无法被捕获到的,main 函数无从得知。

2.select 还是一直在等待

三.在不知道什么时候会停止的情况下,不要开始一个goroutine

例子三

func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
	s := http.Server{
		Addr:    addr,
		Handler: handler,
	}
    
   //开启一个协程接受 关闭信号 进行关闭
	go func() {
		<-stop // wait for stop signal
		s.Shutdown(context.Background())
	}()

	return s.ListenAndServe()
}

func serveApp(stop <-chan struct{}) error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	return serve("0.0.0.0:8080", mux, stop)
}

func serveDebug(stop <-chan struct{}) error {
	return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
}

func main() {
    //done 记录返回的错误
	done := make(chan error, 2)
    //定义终止信号
	stop := make(chan struct{})
   //开启 debug
	go func() {
		done <- serveDebug(stop)
	}()
    //开启主程序
	go func() {
		done <- serveApp(stop)
	}()

	var stopped bool  //只记录1次
    
    //因为有两个协程 所有循环2
	for i := 0; i < cap(done); i++ {
        //当其中一个返回error 时 会打印错误
		if err := <-done; err != nil {
			fmt.Println("error: %v", err)
		}
        //并通知协助进行关闭
		if !stopped {
			stopped = true
			close(stop)
		}
	}
}

每次我们在 done 通道上接收到一个值,我们就关闭 stop 通道,这会导致所有 goroutine 等待该通道关闭它们的 http。这反过来会导致所有剩余的列表和服务 goroutine 返回。一旦我们启动的所有 goroutine 都停止了,main.main 返回并且进程完全停止。

这个代码还是有一些缺陷的,server 里没有进行 错误捕获 Shutdown的错误 还是有可能停止失败的

四 goroutine 泄露

例子一

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/pprof"
)

func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
	s := http.Server{
		Addr:    addr,
		Handler: handler,
	}

	//开启一个协程接受 关闭信号 进行关闭
	go func() {
		<-stop // wait for stop signal
		s.Shutdown(context.Background())
	}()

	return s.ListenAndServe()
}

func serveApp(stop <-chan struct{}) error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	mux.HandleFunc("/received", func(resp http.ResponseWriter, req *http.Request) {
		
		//泄露代码
		ch := make(chan int)
		//这个协程一直在等待 ch 发送的值 但是没有人会发 造成泄露
		go func() {
			val := <-ch
			fmt.Println("We received a value:", val)
		}()
		
		//请求这个链接会直接返回
		fmt.Fprintln(resp, "Hello, received!")
	})
	return serve(":8080", mux, stop)
}

func serveDebug(stop <-chan struct{}) error {
	pprofHandler := http.NewServeMux()
	pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
	return serve(":8001", pprofHandler, stop)
}

func main() {
	//done 记录返回的错误
	done := make(chan error, 2)
	//定义终止信号
	stop := make(chan struct{})
	//开启 debug
	go func() {
		done <- serveDebug(stop)
	}()
	//开启主程序
	go func() {
		done <- serveApp(stop)
	}()

	var stopped bool //只记录1次

	//因为有两个协程 所有循环2
	for i := 0; i < cap(done); i++ {
		//当其中一个返回error 时 会打印错误
		if err := <-done; err != nil {
			fmt.Println("error: %v", err)
		}
		//并通知协助进行关闭
		if !stopped {
			stopped = true
			close(stop)
		}
	}
}

/received 请求这个url 测试泄露

下面是,请求这个链接多次 和最初的对比

 没有请求这个url时的协程数量 只有8个

 但是多请求几次就会出现 很多 协程无法退出 造成泄露

确保创建出的 goroutine 的工作已经完成 

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/pprof"
	"time"
)

func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
	s := http.Server{
		Addr:    addr,
		Handler: handler,
	}

	//开启一个协程接受 关闭信号 进行关闭
	go func() {
		<-stop // wait for stop signal
		s.Shutdown(context.Background())
	}()

	return s.ListenAndServe()
}

func serveApp(stop <-chan struct{}) error {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
		fmt.Fprintln(resp, "Hello, QCon!")
	})
	mux.HandleFunc("/received", func(resp http.ResponseWriter, req *http.Request) {

		//泄露代码
		ch := make(chan int)
		//这个协程一直在等待 ch 发送的值 但是没有人会发 造成泄露
		go func() {
			val := <-ch
			fmt.Println("We received a value:", val)
		}()

		//请求这个链接会直接返回
		fmt.Fprintln(resp, "Hello, received!")
	})

	mux.HandleFunc("/index", func(resp http.ResponseWriter, req *http.Request) {
		
		go func() {
			 time.Sleep(time.Second * 5)  //进行上报信息
			fmt.Println("请求上报信息")
		}()

		//请求这个链接会直接返回
		fmt.Fprintln(resp, "Hello, index!")
	})
	return serve(":8080", mux, stop)
}

func serveDebug(stop <-chan struct{}) error {
	pprofHandler := http.NewServeMux()
	pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
	return serve(":8001", pprofHandler, stop)
}

func main() {
	//done 记录返回的错误
	done := make(chan error, 2)
	//定义终止信号
	stop := make(chan struct{})
	//开启 debug
	go func() {
		done <- serveDebug(stop)
	}()
	//开启主程序
	go func() {
		done <- serveApp(stop)
	}()

	var stopped bool //只记录1次

	//因为有两个协程 所有循环2
	for i := 0; i < cap(done); i++ {
		//当其中一个返回error 时 会打印错误
		if err := <-done; err != nil {
			fmt.Println("error: %v", err)
		}
		//并通知协助进行关闭
		if !stopped {
			stopped = true
			close(stop)
		}
	}
}

这个例子加了一个index 的url 里面进行的一些上报信息的操作

问题1.如果这个上报一直等待 就会造成泄露 

问题2.你永远也不知道什么时候停止



总结

  1. 请将是否并发调用的选择权交给调用者。
  2. 请你对开启的协程负责。2.1 永远不要开启一个你不知道什么时候退出的协程。2.2要管控这个协程的生命周期 知道什么嘛时候退出。不管是chen 还是 context 
  3. 尽量避免在请求中开启协程,上报的执行任务可以投递到消息队列或者自己定义worker里避免协程的泄露 

一些大佬的文献

Practical Go: Real world advice for writing maintainable Go programs

Concurrency Trap #2: Incomplete Work

https://www.ardanlabs.com/blog/2018/11/goroutine-leaks-the-forgotten-sender.html

https://www.ardanlabs.com/blog/2014/01/concurrency-goroutines-and-gomaxprocs.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值