Jochen的goland小抄-进阶篇-并发编程(更新中......)

小抄系列进阶篇涉及的概念较多,知识点重要,故每块知识点独立成篇,方便日后笔记的查询

本篇的主题是:并发编程

go语言在当初推出时,最大的亮点就是其高并发的便捷性,其实现需要依靠协程(有的需要需要靠线程、进程)

什么是并发

我们前面写的程序都是从上至下按顺序执行的,像这样的程序如果遇到了需要一些耗时操作,就会傻不棱噔的等着操作结束后再往下执行,这样的程序执行方式我们称之为“串行”或者“同步”

反之,如果让程序遇到耗时程序时,不傻傻等待,而是一边让它去执行,一边让它接着往下做别的事情,这样的程序执行方式我们称之为“并发”或者"异步"

并发编程就是学习怎么让程序一边做一件事,又一边做另外一件事的方法

首先要理解进程、线程和协程的概念,这部分理论网上比比皆是,不过多叙述,这里只简单介绍下它们

  • 进程:可以简单理解为“正在执行的程序”,系统资源分配的最小单位
    进程一般由程序(我们代码写的就是进程要完成哪些功能已经怎么去完成)、数据集(程序执行过程所需要的资源,如内存分配等)、进程控制块(系统感知进程存在的唯一表示,用来记录进程的状态过程,系统利用其控制控制和管理进程)组成
    ps: 创建、销毁和切换进程的开销是很大的

  • 线程:轻量级进程,是系统的最小执行单位
    一个进程可以包含多个线程,也就是说一个进程内的线程是可以共享内部资源的,大大节省了程序并发执行的开销。但是线程没有自己的系统资源,只能拥有在运行时必不可少的资源
    进程好比一个流水线车间,多个程序在不同的流水线车间上并行(同时)流转着,生产不同的产品,每个车间内的工人就是线程,每个工人只能使用其所在流水线车间内的资源

  • 协程(Coroutine):轻量级线程,也叫做微线程
    这是一种用户态的轻量级线程,用户态线程可以简单理解为协程的一些和信息记录、状态控制(如上下文切换)需要由用户自己去管理,所以协程的调度完全由用户控制
    协程最大的优势就是轻量级,因为其调度与子程序(函数)的切换完全由用户(程序自身)去分配和管理(用代码控制),所以几乎不耗费系统资源
    协程一般与子程序(函数)比较理解,函数调用总有一个入口,一次返回,一旦退出,就完成了子程序的执行

go语言并发是依靠协程实现的。与进程和线程相比,协程的优势在与其轻量级,可以轻松的创建上百万个协程而不会导致系统资源的衰减(进程和线程通常最多不会创建超过1w个)

并发性(Concurrency)

go语言是并发语言,而不是并行语言,因为其实现异步的方式是通过协程(在go被称为Goroutine

并发与并行的区别:

  • 并发和并行都是指的同时处理许多事情的能力,这里的同时指定是一个时间段内同时处理多个任务的意思
  • 并发性,指的是同一时间点上只执行一个任务。但是在同一时间段来看,其同时的在处理多个任务,这是因为任务在一段时间内交替执行的(cpu执行速度太快让我们感觉任务就是一起执行的)。并发主要针对的是cpu单核上执行的任务
  • 并行性,指的是同一时间点可以同时执行多个任务。在同一段时间,其也是同时在处理多个任务,但是任务是一直地同时执行的。不同的任务并行执行需要由cpu不同核心同时执行

所以可以看到,真正的并行是需要靠多核的硬件支持的,如果电脑是单核的就谈不上并行

并行并非意味着更快的执行时间,因为并行运行的程序之间往往需要通信,在多个核心上运行的程序之间进行通信开销是远远大于单核并发执行的程序之间的通信

GoRoutine

协程的英文名为Coroutine,go语言中的为自己的“协程”命名为Goroutine,它是go语言的专有名词

也就是说GoRoutine就是go语言的协程,go语言是通过Goroutine来实现并发的

  • 使用并发往往是同时执行几个任务,每个任务对应到程序中就是某个函数的代码,所有可以理解为Goroutine的作用就是执行某个函数和某个方法
  • Goroutines是一个函数或方法,它与其他函数或方法同时运行
  • Goroutine是轻量级的线程,与线程相比,创建Goroutine的成本极低:它就是一段代码,一个函数的入口。只需要在堆上为其分配一个堆栈(初始大小为4k,随着程序的执行可自动增删),而线程堆栈的大小必须指定并且固定
  • Go应用程序可以并发运行数个Goroutines

主goroutine

main函数中若调用了goroutine,则该main函数称为主goroutine,也叫做主协程(不是执行main函数的goroutine就叫做子协程)

主协程不单只是简单执行main函数如此简单:

  1. 首先它会设定goroutine所能申请的最大栈空间(32位os为250M,64位为1G,若goroutine的栈空间大于最大尺寸限制,运行时会引发stack overflow的运行时恐慌,程序终止)
  2. 接下来主goroutine的会进行初始化操作:
    • 创建特殊的defer语句,用于在主协程非正常退出时做善后处理
    • 启用一个用于在后台清扫内存垃圾的goroutine,设置GC可用的标识
    • 执行main包中的init函数
    • 执行main函数,执行结束后主协程结束自己和当前进程的运行(此时若有子协程还在运行,则强制结束)

使用goroutine

go语言把并发编程简直简化成了傻瓜式操作,舒服的一批,通过代码看吧:

package main

import (
	"fmt"
	"time"
)

func main() {
	/*
		使用Goroutines:
			在函数或方法调用前加上关键字go,就会同时运行一个新的Goroutine
			Goroutine执行的函数往往是没有返回值的,即使有也往往会被舍弃
	*/

	//案例:使用一个goroutine打印英文,另一个goroutine打印中文,观察运行结构

	//1.创建子协程,执行printEnglish()函数
	go printEnglish()

	//2.main函数中打印中文,因为main函数中使用了goroutine,所以此时main函数叫做主goroutine
	for i := 0; i < 50; i++ {
		fmt.Println("主goroutine打印中文:", "帅", i)
	}

	/*
		输出结果发现:
			1.主协程和子协程交替执行(每次执行都不一样,因为什么时候执行哪个协程是不缺地你过的)
			2.若主协程执行结束,则子协程直接提前拉闸(结束)
	*/
	time.Sleep(2 * time.Second) //主函数结束后睡个两秒再结束,避免主协程执行完提前终止子协程
	fmt.Println("main over..")
}

func printEnglish() int {
	for i := 0; i < 100; i++ {
		fmt.Println("子goroutine打印英文:", "goland is the best language!", i)
	}
	return 1
}

func init() {
	fmt.Println("执行init")
}

Goroutine规则须知:

  • 当一个新的goroutine开始时,goroutine调用立即返回。与普通函数调用不同的是:goroutine的所有返回值都会被忽略,且go不等待goroutine执行结束,当调用后立即继续往下执行调用处后面的代码
  • main函数goroutine应该为其他的goroutines执行,如果主协程终止,则程序终止,其他的子协程将提前会全部拉闸

本系列学习资料参考:
https://www.bilibili.com/video/BV1jJ411c7s3?p=15
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter01/01.1.html

上一篇:387集Go语言核心编程培训视频教材整理 | goroutine和channel(四)


下一篇:深入了解Golang网络库中socket阻塞调度源码