[TOC]
并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求,这也是并发的必要性之一。Golang的并发控制比起Java来说,简单了不少。在Golang中,没有多线程这一说法,只有协程,而新建一个协程,仅仅只需要使用go关键字。而且,与Java不同的是,在Golang中不以共享内存的方式来通信,而是以通过通信的方式来共享内存。这方面的内容也比较简单。

1 线程与协程

在Golang中,并发是以协程的方式实现的。

在Java中,我们常常提到线程池,多线程这些概念。然而,在Golang中的协程,和这些是不一样的。所以在本文中,先对这几个概念进行区分。

简单来说,进程和线程是由操作系统进行调度的,协程是对内核透明,由程序自己调度的。不仅如此,Golang的协程所占用的内存空间极小,也就是说,协程更加的轻量。此外,协程的切换一般由程序员在代码中显式控制,而不是交给操作系统去调度。它避免了上下文切换时的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。

至于别的,本文不进行深入的研究,本文的基调还是以入门为主,即怎么去用。

2 goroutine

简单来说,我们所编写的Golang源代码全部都跑在goroutine中。

我们只需要使用go关键字,就可以启动一个goroutine。

package main
import "fmt"

func f(msg string) {
    fmt.Println(msg)
}

func main(){
    go f("hello goroutine")
}

至于其余的事情,就交给Golang的runtime了,Go的runtime负责对goroutine进行调度。简单的来讲,调度就是决定哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等。

我们下面写个小例子,来看看Golang如何编写并发的小程序:

package main

import (
    "io"
    "log"
    "net"
    "time"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err) // 假设出现了错误
            continue
        }
        handleConn(conn) // 处理连接
    }
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05n"))
        if err != nil {
            return // 连接关闭,则停止执行
        }
        time.Sleep(1 * time.Second)
    }
}

简单解释一下,这个来自于这里的小例子中,我们监听了本地8000端口的TCP连接。然后,当有连接过来的时候,每隔一秒将当前的时间打印在屏幕上。

但是问题来了,如果我们再打开一个CMD窗口,去建立一个TCP连接,是失败的。除非将原来的那个连接中断,Golang才能接受新的连接。不然,新的连接将一直被阻塞。

在这个时候,我们只需要在调用handleConn(conn)这个函数之前,加上go的关键字,就可以实现并发了。

for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err) // 假设出现了错误
            continue
        }
        go handleConn(conn) // 处理连接
    }

随后,我们就可以处理多个连接了:

所以,在Golang中实现并发,就是这么的简单。我们需要做的,就是在调用需要创建协程的函数前面,加上go关键字。

3 channel

注意,在Golang的并发中有一项很重要的特性,不要以共享内存的方式来通信,相反,要通过通信来共享内存。

这里说到的通信方式,指得就是channel,信道。

Channel是Go中的一个核心类型,我们可以把理解为是一种指定了大小和容量的管道。我们可以在这个管道的一边放入数据,在另一半拿出数据。举个简单的例子:

package main

import "fmt"

func main() {
   
   messages := make(chan string)

   go func() { messages 

在这里需要说明几点:

  • 信道需要使用make的方式创建,除了能够指定类型,还能在第二个参数指定容量,否则默认为1,也就是说这是一个同步信道
  • 消息的传递和获取必须成对出现,传数据用channel
  • 信道是会阻塞的,而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
  • 对于阻塞,可以理解为是一个管道中已经有了东西,那么只有管道为空了,才能继续工作

4 range

对于上面提到的信道操作,存在这么几个问题:

  1. 应该何时停止等待数据?
  2. 还会有更多的数据么,还是所有内容都已经传输完成?
  3. 我应该继续等待还是该做别的了?

当然,我们可以选择不断检查信道,直到他关闭为止。

但是我们有更加优雅的解决方案。使用range关键字,使用在channel上时,会自动等待channel的动作一直到channel被关闭。下面来看一个小例子,这个例子来源于简书:

package main                                                                                             
import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, count int) {
    for i := 1; i 

在这里,我们定义了一个同步信道。

在制作蛋糕的过程中,我们使用了一个for循环,不断的将蛋糕送入cs中。

注意,这里因为是同步信道,所以并不是将五个蛋糕全部制作完,再全部一起接收的,而是制作一个,接受一个。

最后,我们关闭这个信道,随后range发现信道被关闭,于是结束。这也就实现了接收器不知道具体需要接收多少个蛋糕的情况下,能够自动结束的功能。

5 select

select关键字用在有多个信道的情况下。

他的目的是为了提高系统的效率,而不至于在某一个信道阻塞的情况下,不知道该干什么。

select中会有case代码块,用于发送或接收数据。语法如下:

select {
case i := 

注意,每一个case,必须是一个信道IO指令,default命令块不是必须。

规律如下:

  • 如果任意一个case代码块准备好发送或接收,执行对应内容
  • 如果多余一个case代码块准备好发送或接收,随机选取一个并执行对应内容
  • 如果任何一个case代码块都没有准备好,等待
  • 如果有default代码块,并且没有任何case代码块准备好,执行default代码块对应内容

我们还是以上面做蛋糕为例,但是这次可以同时做草莓味和巧克力味的蛋糕了:

package main

import (
    "fmt"
    "strconv"
    "time"
)

func makeCakeAndSend(cs chan string, flavor string, count int) {
    for i := 1; i 

在这里,因为我们是不知道哪种口味的蛋糕已经被制作完成的,所以我们使用了select。只要这个case被激活了,那么就会完成后面的代码。也就是说,当某种口味的蛋糕被制作完成之后,就会被收取。

注意,我们这里使用的多个返回值

case cakeName, strbry_ok := 

第二个返回值是一个bool类型,当其为false时说明channel被关闭了。如果是true,说明有一个值被成功传递了。

我们使用可以这个值来判断是否应该停止等待。

由于版权原因,本站共享资源只供云盘资源,版权均属于影片公司所有,请在下载后24小时删除,切勿用于商业用途。本站所有资源信息均从互联网搜索而来,本站不对显示的内容承担责任,如您认为本站页面信息侵犯了您的权益,请附上版权证明邮件并发送到xkdadmin@163.com告知,我们会在收到邮件后72小时内删除。
想开点 » go 并发