非零基础自学Golang 第13章 并发与通道 13.2 goroutine 13.2.1 goroutine定义 & 13.2.2 创建goroutine & 13.2.3 main 函数

非零基础自学Golang

第13章 并发与通道

13.2 goroutine

goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。

不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。

13.2.1 goroutine定义

在Go语言中,每一个并发的执行单元叫作一个goroutine。

想要编写一个并发任务,只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。

go 函数名(函数参数)

如果函数有返回值,返回值会被忽略。因此,一旦使用go关键字,就不能使用函数返回值来与主进程进行数据交换,而只能使用channel,关于channel后面再细说。

对用户来说,协程与线程几乎没什么区别,但是实际上二者是有一定区别的。

线程有固定的栈,基本都是2 MB,都是固定分配的;这个栈用于保存局部变量,在函数切换时使用。但是对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费,所以Go采用了动态扩张收缩的策略,初始化为2 KB,最大可扩张到1 GB

【提示】

操作系统对线程进行调度也需要成本。线程挂起前会保存线程上下文到栈空间,再切换到可执行线程。线程数很多时,线程上下文切换会导致CPU开销变大。

每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便地通过id操作某个线程。

但是goroutine内没有这个概念,这是在Go语言设计之初出于防止被滥用的考虑而定的,所以你不能在一个协程中“杀死”另外一个协程,编码时需要考虑到协程什么时候创建以及什么时候释放。

协程和线程最重要的区别在于:

首先,线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这样代价就小了。

其次,协程的切换时间点是由调度器决定,而不是由系统内核决定的,尽管它们的切换点都是时间片超过一定阈值,或者是进入I/O或睡眠等状态时。

最后,基于垃圾回收的考虑,Go实现了垃圾回收,但垃圾回收的必要条件是内存位于一致状态,因此就需要暂停所有的线程。如果交给系统去做,那么会暂停所有的线程使其一致;但如果在Go里,调度器知道什么时候内存位于一致状态,也就没有必要暂停所有运行的线程。

13.2.2 创建goroutine

只需要在函数调用语句前面添加go关键字,就可以创建并发执行单元。

开发人员无须了解任何执行细节,调度器会自动将其安排到合适的系统线程上去执行。【牛逼!!】

在并发编程里,我们通常要将一个过程分割成几块,然后让不同的goroutine各自负责其中的一块工作。

[ 动手写 13.2.1]

package main

import (
   "fmt"
   "time"
)

func Task1() {
   for {
      fmt.Println(time.Now().Format("15:04:05"), "正在处理Task 1 的任务!")
      time.Sleep(time.Second * 3)
   }
}

func Task2() {
   for {
      fmt.Println(time.Now().Format("15:04:05"), "正在处理Task 2 的任务!")
      time.Sleep(time.Second * 1)
   }
}

func main() {

   go Task1()
   go Task2()

   for {
      fmt.Println(time.Now().Format("15:04:05"), "正在处理主进程 的任务!")
      time.Sleep(time.Second * 2)
   }
}

运行结果

在这里插入图片描述

如果将main中的两个关键字删除,由于Task1是死循环,就会一直执行循环里的内容,导致执行不到其他Task,因此程序运行的结果如下:

在这里插入图片描述

13.2.3 main 函数

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们称之为main goroutine。

新的goroutine会使用go来创建。先来看一个例子:

[ 动手写13.2.2]

package main

import (
   "fmt"
   "time"
)

func Task1() {

   for {
      fmt.Println(time.Now().Format("15:04:05"), "正在处理Task1 的任务!")
      time.Sleep(time.Second * 3)
   }
}

func main() {
   go Task1()
}

运行结果

在这里插入图片描述

动手写13.2.2运行后将不会有任何输出,这是因为运行go Task1(),程序会立刻返回到main函数,而main函数后面没有任何代码逻辑,程序就会判断为执行完毕,终止所有协程。

因此,想要让Task1函数执行,可以在main函数添加一些等待逻辑,例如Sleep()。

[ 动手写 13.2.3 ]

package main

import (
   "fmt"
   "time"
)

func Task1() {

   for {
      fmt.Println(time.Now().Format("15:04:05"), "正在处理Task1 的任务!")
      time.Sleep(time.Second * 3)
   }
}

func main() {
   go Task1()
   time.Sleep(time.Second * 100)
}

运行结果

在这里插入图片描述

【提示】

所有goroutine在main函数结束时会一并结束。goroutine虽然类似于线程概念,但调度性能上不如线程细致,而细致程度取决于goroutine调度器的实现和运行环境。

终止goroutine最好的方法是直接在函数中自然返回。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ding Jiaxiong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值