golang并发编程实战[1]

slarsar · · 885 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

什么是进程(process)

进程(process),是指计算机中已运行的程序。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机体系结构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。

什么是线程 (thread)

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见的,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

并发和并行 (concurrency & parallelism)

并发:concurrency, 并行:parallelism. 并发和并行是完全不同的两个概念但是却相互有关联,并发是指在一时间段内执行很多个过程, 一个cpu上能同时执行多项任务,在很短时间内,cpu来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。而并行是多个进程在多个cpu上同时执行,并行程序各自有各子的cpu资源,并发程序单核就可以完成,而并行程序必须要多核才能完成, 我的理解是:并行是并发的子集.

什么是协程 (goroutine)

一个Goroutine是一个与其他goroutines 并发运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。它与线程、协程、进程等不同。它是一个goroutine

Goroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真实的Go语言的实现中,goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别,但正是这个量变引发了Go语言并发编程质的飞跃。

Do not communicate by sharing memory; instead, share memory by communicating. 不要通过共享内存来通信,相反,通过通信来共享内存。

goroutine vs thread

每个系统级线程都会有一个固定大小的栈(一般默认可能是2MB),这个栈主要用来保存函数递归调用时参数和局部变量。固定了栈的大小导致了两个问题:一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费,二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是:要么降低固定的栈大小,提升空间的利用率;要么增大栈的大小以允许更深的函数递归调用,但这两者是没法同时兼得的。相反,一个Goroutine会以一个很小的栈启动(可能是2KB或4KB),当遇到深度递归导致当前栈空间不足时,Goroutine会根据需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。

Go的运行时还包含了其自己的调度器,这个调度器使用了一些技术手段,可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度,只有在当前Goroutine发生阻塞时才会导致调度;同时发生在用户态,调度器会根据具体函数只保存必要的寄存器,切换的代价要比系统线程低得多。运行时有一个runtime.GOMAXPROCS变量,用于控制当前运行正常非阻塞Goroutine的系统线程数目。

在Go语言中启动一个Goroutine不仅和调用函数一样简单,而且Goroutine之间调度代价也很低,这些因素极大地促进了并发编程的流行和发展。

示例程序


func main() {
    go println("Go! Goroutine!")
}

运行的结果是什么也不会发生,因为main线程早早就退出了,是不会等待go prinln()的。


func main() {
  go println("Go! Goroutine!")
  time.Sleep(time.Second)
}

等待一秒main函数再退出, 可以看到goroutine的打印输出了

func main() {
    names := []string{"Eric", "Harry", "Robert", "Jim", "Mark"}
    for _, name := range names {
        go func() {
            fmt.Printf("Hello, %s!\n", name)
        }()
    }
    time.Sleep(time.Millisecond)
}
$ go run gobase4.go 
Hello, Mark!
Hello, Mark!
Hello, Mark!
Hello, Mark!
Hello, Mark!
func main() {
    names := []string{"Eric", "Harry", "Robert", "Jim", "Mark"}
    for _, name := range names {
        go func(who string) {
            fmt.Printf("Hello, %s!\n", who)
        }(name)
    }
    time.Sleep(time.Millisecond)
}

将 go 启动的函数携带上name参数,输出是这样的:

$ go run gobase5.go 
Hello, Harry!
Hello, Eric!
Hello, Jim!
Hello, Mark!
Hello, Robert!

runtime.Gosched()

package main

import (
    "runtime"
)

func say(x string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        print(i); println(x)
    }
}

func main() {
    go say("hello")
    say("world")
}
$ go run gobase2.go 
world
hello
world
hello
world
hello
world
hello
world

这就很奇怪了, runtime.Gosched()在这里到底有什么影响?

什么是Gosched()

/usr/lib/go/src/runtime/包下的的注释:

Gosched yields the processor, allowing other goroutines to run. It does not suspend the current goroutine, so execution resumes automatically.

stakoverflow关于Gosched的回答

再看一下
go语言中文网一个博主的简明解释

runtime 中的 GOMAXPROCS

func GOMAXPROCS(n int) int
GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting. If n < 1, it does not change the current setting. The number of logical CPUs on the local machine can be queried with NumCPU. This call will go away when the scheduler improves.

run.GOMAXPROCS(0)本来是设置MAXPROCS的,但是如果这个函数的参数是0,则不会做任何改动,并且返回当前的MAXPROCS值.

    println(runtime.NumCPU())

我执行上面程序的结果是12,因为我所使用的设备有6个物理cpu核心,12个逻辑CPU核心,go默认的MAXPROCS是逻辑CPU的数量.

我先设置GOMAXPROCS为1时的情况:

得到而输出:

$ go run gobase2.go
0world
1world
0hello
2world
1hello
3world
2hello
4world

设置GOMAXPROCS为2时:

$ go run gobase2.go
0world
10hello
1hello
2hello
3hello
4hello
world
2world
3world
4world

time

time.NewTimer(A) 返回一个Timer类型的实例,这个实例会通过它的管道在A时间段过后发送当前时刻时间
NewTimer creates a new Timer that will send the current time on its channel after at least duration d.

func main() {
    intChan := make(chan int, 1)
    go func() {
        time.Sleep(time.Second)
        intChan <- 1
    }()
    aTimer := time.NewTimer(time.Millisecond * 500)
    select {
    case e := <-intChan:
        fmt.Printf("Received: %v\n", e)
    case timerNow := <- aTimer.C:
        fmt.Println(timerNow,"is currenttime")
        fmt.Println("Timeout!")
    }
}

返回:


$ go run chantimeout1.go 
2019-08-28 20:33:35.81535456 +0800 CST m=+0.500175601 is currenttime
2019-08-28 20:33:35.815899839 +0800 CST m=+0.500720915  is time now 
Timeout!

time.Reset(timeout) 会重置定时器


func main() {
    intChan := make(chan int, 1)
    go func() {
        for i := 0; i < 5; i++ {
            time.Sleep(time.Second)
            intChan <- i
        }
        close(intChan)
    }()
    timeout := time.Millisecond * 200
    var timer *time.Timer
    for {
        if timer == nil {
            timer = time.NewTimer(timeout)
        } else {
            timer.Reset(timeout)
        }
        select {
        case e, ok := <-intChan:
            if !ok {
                fmt.Println("End.")
                return
            }
            fmt.Printf("Received: %v\n", e)
        case <-timer.C:
            fmt.Println("Timeout!")
        }
    }

}

输出:

$ go run chantimeout2.go                                                                                                                    
Timeout!
Timeout!
Timeout!
Timeout!
Received: 0
Timeout!
Timeout!
Timeout!
Timeout!
Received: 1
Timeout!
Timeout!
Timeout!
Timeout!
Received: 2
Timeout!
Timeout!
Timeout!
Timeout!
Received: 3
Timeout!
Timeout!
Timeout!
Timeout!
Received: 4
End.


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:slarsar

查看原文:golang并发编程实战[1]

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

885 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传