golang高并发的深入理解

 更新时间:2019年03月10日 15:29:10   作者:飞翔码农  
golang从语言级别上对并发提供了支持,而且在启动并发的方式上直接添加了语言级的关键字。下面这篇文章主要给大家介绍了关于golang高并发的相关资料,需要的朋友可以参考下

前言

GO语言在WEB开发领域中的使用越来越广泛,Hired 发布的《2019 软件工程师状态》报告中指出,具有 Go 经验的候选人是迄今为止最具吸引力的。平均每位求职者会收到9 份面试邀请。


想学习go,最基础的就要理解go是怎么做到高并发的。

那么什么是高并发?

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

严格意义上说,单核的CPU是没法做到并行的,只有多核的CPU才能做到严格意义上的并行,因为一个CPU同时只能做一件事。那为什么是单核的CPU也能做到高并发。这就是操作系统进程线程调度切换执行,感觉上是并行处理了。所以只要进程线程足够多,就能处理C1K C10K的请求,但是进程线程的数量又受到操作系统内存等资源的限制。每个线程必须分配8M大小的栈内存,不管是否使用。每个php-fpm需要占用大约20M的内存。所以目前有线程的Java就比只有进程的PHP的并发处理能力高。当然了,软件的处理能力不仅仅跟内存有关,还有是否阻塞,是否异步处理,CPU等等。Nginx作为单线程的模型却可以承担几万甚至几十万的并发请求,Nginx的话题说起来也就更多了。
我们继续聊我们的Go,那么是不是可以有一种语言使用更小的处理单元,占用内存比线程更小,那么它的并发处理能力就可以更高。所以Google就做了这件事,就有了golang语言,golang从语言层面就支持了高并发。

go为什么能做到高并发

goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,几十个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

一些高并发的处理方案基本都是使用协程,openresty也是利用lua语言的协程做到了高并发的处理能力,PHP的高性能框架Swoole目前也在使用PHP的协程。

协程更轻量,占用内存更小,这是它能做到高并发的前提。

go web开发中怎么做到高并发的能力

学习go的HTTP代码。先创建一个简单的web服务。

package main

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

func response(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hello world!") //这个写入到w的是输出到客户端的
}

func main() {
 http.HandleFunc("/", response)
 err := http.ListenAndServe(":9000", nil)
 if err != nil {
  log.Fatal("ListenAndServe: ", err)
 }
}

然后编译

go build -o test_web.gobin
./test_web.gobin

然后访问

curl 127.0.0.1:9000
Hello world!

这样简单的一个WEB服务就搭建起来。接下来我们一步一步理解这个Web服务是怎么运行的,怎么做到高并发的。
我们顺着http.HandleFunc("/", response)方法顺着代码一直往上看。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

type ServeMux struct {
 mu sync.RWMutex//读写锁。并发处理需要的锁
 m  map[string]muxEntry//路由规则map。一个规则一个muxEntry
 hosts bool //规则中是否带有host信息
}
一个路由规则字符串,对应一个handler处理方法。
type muxEntry struct {
 h  Handler
 pattern string
}

上面是DefaultServeMux的定义和说明。我们看到ServeMux结构体,里面有个读写锁,处理并发使用。muxEntry结构体,里面有handler处理方法和路由字符串。

接下来我们看下,http.HandleFunc函数,也就是DefaultServeMux.HandleFunc做了什么事。我们先看mux.Handle第二个参数HandlerFunc(handler)

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 mux.Handle(pattern, HandlerFunc(handler))
}
type Handler interface {
 ServeHTTP(ResponseWriter, *Request) // 路由实现器
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
 f(w, r)
}

我们看到,我们传递的自定义的response方法被强制转化成了HandlerFunc类型,所以我们传递的response方法就默认实现了ServeHTTP方法的。

我们接着看mux.Handle第一个参数。

func (mux *ServeMux) Handle(pattern string, handler Handler) {
 mux.mu.Lock()
 defer mux.mu.Unlock()

 if pattern == "" {
  panic("http: invalid pattern")
 }
 if handler == nil {
  panic("http: nil handler")
 }
 if _, exist := mux.m[pattern]; exist {
  panic("http: multiple registrations for " + pattern)
 }

 if mux.m == nil {
  mux.m = make(map[string]muxEntry)
 }
 mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

 if pattern[0] != '/' {
  mux.hosts = true
 }
}

将路由字符串和处理的handler函数存储到ServeMux.m 的map表里面,map里面的muxEntry结构体,上面介绍了,一个路由对应一个handler处理方法。

接下来我们看看,http.ListenAndServe(":9000", nil)做了什么

func ListenAndServe(addr string, handler Handler) error {
 server := &Server{Addr: addr, Handler: handler}
 return server.ListenAndServe()
}

func (srv *Server) ListenAndServe() error {
 addr := srv.Addr
 if addr == "" {
  addr = ":http"
 }
 ln, err := net.Listen("tcp", addr)
 if err != nil {
  return err
 }
 return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

net.Listen("tcp", addr) ,就是使用端口addr用TCP协议搭建了一个服务。tcpKeepAliveListener就是监控addr这个端口。

接下来就是关键代码,HTTP的处理过程

func (srv *Server) Serve(l net.Listener) error {
 defer l.Close()
 if fn := testHookServerServe; fn != nil {
  fn(srv, l)
 }
 var tempDelay time.Duration // how long to sleep on accept failure

 if err := srv.setupHTTP2_Serve(); err != nil {
  return err
 }

 srv.trackListener(l, true)
 defer srv.trackListener(l, false)

 baseCtx := context.Background() // base is always background, per Issue 16220
 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
  rw, e := l.Accept()
  if e != nil {
   select {
   case <-srv.getDoneChan():
    return ErrServerClosed
   default:
   }
   if ne, ok := e.(net.Error); ok && ne.Temporary() {
    if tempDelay == 0 {
     tempDelay = 5 * time.Millisecond
    } else {
     tempDelay *= 2
    }
    if max := 1 * time.Second; tempDelay > max {
     tempDelay = max
    }
    srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
    time.Sleep(tempDelay)
    continue
   }
   return e
  }
  tempDelay = 0
  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew) // before Serve can return
  go c.serve(ctx)
 }
}

for里面l.Accept()接受TCP的连接请求,c := srv.newConn(rw)创建一个Conn,Conn里面保存了该次请求的信息(srv,rw)。启动goroutine,把请求的参数传递给c.serve,让goroutine去执行。

这个就是GO高并发最关键的点。每一个请求都是一个单独的goroutine去执行。

那么前面设置的路由是在哪里匹配的?是在c.serverde的c.readRequest(ctx)里面分析出URI METHOD等,执行serverHandler{c.server}.ServeHTTP(w, w.req)做的。看下代码

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
 handler := sh.srv.Handler
 if handler == nil {
  handler = DefaultServeMux
 }
 if req.RequestURI == "*" && req.Method == "OPTIONS" {
  handler = globalOptionsHandler{}
 }
 handler.ServeHTTP(rw, req)
}

handler为空,就我们刚开始项目中的ListenAndServe第二个参数。我们是nil,所以就走DefaultServeMux,我们知道开始路由我们就设置的是DefaultServeMux,所以在DefaultServeMux里面我一定可以找到请求的路由对应的handler,然后执行ServeHTTP。前边已经介绍过,我们的reponse方法为什么具有ServeHTTP的功能。流程大概就是这样的。

我们看下流程图


结语

我们基本已经学习忘了GO 的HTTP的整个工作原理,了解到了它为什么在WEB开发中可以做到高并发,这些也只是GO的冰山一角,还有Redis MySQL的连接池。要熟悉这门语言还是多写多看,才能掌握好它。灵活熟练的使用。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

相关文章

  • golang实现的文件上传下载小工具

    golang实现的文件上传下载小工具

    这篇文章主要介绍了golang实现的文件上传下载小工具,帮助大家更好的理解和使用python,感兴趣的朋友可以了解下
    2020-12-12
  • golang有用的库及工具 之 zap.Logger包的使用指南

    golang有用的库及工具 之 zap.Logger包的使用指南

    这篇文章主要介绍了golang有用的库及工具 之 zap.Logger包的使用指南,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 浅谈Go语言的高效编码细节

    浅谈Go语言的高效编码细节

    这篇文章主要介绍了浅谈Go语言的高效编码细节,我们都知道golang是天生的高并发,高效的编译型语言,可我们也都可知道,工具再好,用法不对,全都白费,我们来举2个常用路径来感受一下
    2023-01-01
  • Go语言线程安全之互斥锁与读写锁

    Go语言线程安全之互斥锁与读写锁

    这篇文章主要介绍了Go语言线程安全之互斥锁与读写锁,互斥锁是为了并发的安全,在多个goroutine共同工作的时候,对于共享的数据十分不安全,而读写锁效率革命,使用锁的时候,安全与效率往往需要互相转换,下文详细内容,需要的小伙伴可以参考一下
    2022-02-02
  • 解决Go Json Unmarshal反序列化丢失数字精度问题

    解决Go Json Unmarshal反序列化丢失数字精度问题

    业务会使用 id生成器 产生的 分布式唯一ID,长度比较长,所以代码反序列化时,会出现精度丢失问题,那如何解决呢,下面小编就来和大家详细讲讲
    2023-08-08
  • Golang中errgroup的常见误用详解

    Golang中errgroup的常见误用详解

    errgroup和sync.WaitGroup类似,都可以发起执行并等待一组协程直到所有协程运行结束,本文主要为大家整理了一些errgroup的常见误用,有需要的可以参考下
    2024-01-01
  • go语言实现抓取高清图片

    go语言实现抓取高清图片

    本文给大家分享的是使用go语言实现的抓取高清美女图片的代码,原理非常简单,这里就不多废话了,主要是看到很多小伙伴使用python实现的,心血来潮就用go写了下,推荐给大家。
    2015-03-03
  • 构建Golang应用最小Docker镜像的实现

    构建Golang应用最小Docker镜像的实现

    这篇文章主要介绍了构建Golang应用最小Docker镜像的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • Go map底层实现与扩容规则和特性分类详细讲解

    Go map底层实现与扩容规则和特性分类详细讲解

    这篇文章主要介绍了Go map底层实现与扩容规则和特性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-03-03
  • 一文详解go的defer和return的执行顺序

    一文详解go的defer和return的执行顺序

    go的defer和return是golang中的两个关键字,return用于返回函数的返回值,也可以参与一定的流程控制,defer是golang中的延迟调用,经常用于文件流的关闭,锁的解锁操作,本文给大家介绍了go的defer和return的执行顺序,需要的朋友可以参考下
    2024-07-07

最新评论