Golang并发编程入门教程

时间单位

  • 1S = 1000ms
  • 1ms = 1000us
  • 1us = 1000ns

并发与并行

  • 并行: 借助多核 cpu 实现。 (真 并行)
  • 并发:
    • 宏观:用户体验上,程序在并行执行。
    • 微观:多个计划任务,顺序执行。在飞快的切换。轮换使用 cpu 时间轮片。 【假 并行】

进程并发

  • 程序:编译成功得到的二进制文件。
    • 占用磁盘空间
    • 死的
    • 系统中相同的程序一般只有一个,有可能存在不同版本号的程序
  • 进程:运行起来程序。 占用系统资源。
    • 进程是根据程序创建的,运行在内存中
    • 活的
    • 一个程序可以启动N个进程
  • 一个程序可以启动N个进程,程序和进程的关系是1对N的关系。

进程状态

  • 初始态
  • 就绪态:程序会经常从就绪态切换到运行态。
  • 运行态:只有运行态能切换到阻塞态。
  • 挂起(阻塞)态:阻塞态可以切换回运行态继续往下执行。
  • 终止(停止)态

线程并发

  • 线程:LWP 轻量级的 进程。最小的执行单位。是cpu分配时间轮片的对象。
  • 进程: 最小的系统资源分配单位。

同步

  • 协同步调。规划先后顺序。
  • 线程同步机制:
    • 互斥锁(互斥量):建议锁。拿到锁以后,才能访问数据,没有拿到锁的线程,阻塞等待。等到拿锁的线程释放锁。
    • 读写锁:一把锁(读属性、写属性)。 写独占,读共享。 写锁优先级高。

协程并发

  • Python、Lua、Rust。。。
  • 21世纪
  • 线程会io阻塞,协程可以在线程io阻塞期间相互切换,执行其他不需要消耗io操作的协程
  • 提高程序执行的效率

总结

  • 进程、线程、协程 都可以完成并发。
  • 稳定性强、节省资源、效率高。
  • 老板——手机:
    • 生产线 —— 设备、材料、厂房 —— 进程。(资源分配单位)
    • 工人 —— 线程。 —— 单进程、单线程的 程序。
    • 50 工人 —— 50 线程。 ——单进程、多线程的 程序。
    • 10 条生产线 —— 500 工人 —— 。多进程、多线程的 程序。
    • 利用闲暇时间义务搬砖 —— 协程。—— 多进程、多线程、多协程 程序。

创建Goroutine程

  • 创建于进程中。 直接使用 go 关键,放置于 函数调用前面,产生一个 go程。 并发。

示例代码:

package main

import (
	"fmt"
	`time`
)

// 唱歌
func sing()  {
	for i:=0;i<50;i++ {
		fmt.Println("----正在唱:隔壁泰山----")
	}
}

// 跳舞
func dance()  {
	for i:=0;i<50;i++ {
		fmt.Println("----正在跳舞:赵四街舞----")
	}
}

func main()  {
	go sing() // 开启goroutine
	go dance() // 另一个goroutine,会抢夺CPU执行权,两个goroutine交替执行
	
	time.Sleep(time.Second)
}

Goroutine的特性:【重点】

  • 主go程结束,子go程随之退出。

示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	
	go func() { // 创建一个 子go 程
		for i := 0; i < 5; i++ {
			fmt.Println("------I'm goroutine -------")
			time.Sleep(time.Second)
		}
	}()
	
	// 主goroutine结束,子goroutine随着退出
	// 所以,子goroutine很有可能没有执行完就退出了
	fmt.Println("------I'm main-------")
}

runtime.Gosched()

  • 出让当前go程所占用的 cpu时间片。当再次获得cpu时,从出让位置继续回复执行。
  • 时间片轮转调度算法。

示例代码:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	
	go func() {
		for {
			runtime.Gosched()
			fmt.Println("goroutine 1执行。。。。")
		}
	}()
	
	for {
		runtime.Gosched() // 出让当前 cpu 时间片。
		fmt.Println("主go程执行。。。。")
	}
}

runtime.Goexit()

  • return: 返回当前函数调用到调用者那里去。 return之前的 defer 注册生效。
  • Goexit(): 结束调用该函数的当前go程。Goexit():之前注册的 defer都生效。

示例代码:

package main

import (
	"fmt"
	"runtime"
	`time`
)

func test() {
	defer fmt.Println("退出子go程之前")
	// return
	runtime.Goexit() // 退出当前go程。
	defer fmt.Println("退出子go程之后")
}

func main() {
	fmt.Println("主go程开始执行")
	
	go func() {
		fmt.Println("父go程执行之前")
		
		// runtime.Goexit() 会确保defer执行
		defer fmt.Println("defer 父go程执行之后")
		go test()
		
		fmt.Println("父go程执行之后")
	}()
	
	fmt.Println("主go程结束执行")
	time.Sleep(time.Second)
}

runtime.GOMAXPROCS()

  • 设置当前 进程使用的最大cpu核数。 返回 上一次调用成功的设置值。 首次调用返回默认值。

示例代码:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	
	fmt.Println(runtime.GOROOT())
	
	n := runtime.GOMAXPROCS(0) // 0表示最大
	fmt.Println("n = ", n)     // 返回上次设置成功的设置值
	
	n = runtime.GOMAXPROCS(2)
	fmt.Println("n = ", n) // 16
	
	n = runtime.GOMAXPROCS(4)
	fmt.Println("n = ", n) // 2
	
	n = runtime.GOMAXPROCS(8)
	fmt.Println("n = ", n) // 4
	
	n = runtime.GOMAXPROCS(1)
	fmt.Println("n = ", n) // 8
}

补充知识点

  • 每当有一个进程启动时,系统会自动打开三个文件: 标准输入、标准输出、标准错误。 —— 对应三个文件: stdin、stdout、stderr
  • 当进行运行结束。操作系统自动关闭三个文件。

channel

  • 是一种数据类型。 对应一个“管道”(通道 FIFO)
  • channel的定义:
    • make (chan 在channel中传递的数据类型, 容量)
      • 容量= 0: 无缓冲channel
      • 容量 > 0 :有缓冲channel
    • make(chan int) 或 make (chan string , 0)
  • channel有两个端:
    • 一端:写端(传入端) chan <-
    • 另一端: 读端(传出端)<- chan
    • 要求:读端和写端必须同时满足条件,才在shan上进行数据流动。否则,则阻塞。
  • channel是一种先进先出(FIFO)的队列类型

示例代码

package main

import (
	"fmt"
	"time"
)

// 全局定义channel, 用来完成数据同步
var flagChan = make(chan struct{}) // 空struct不占用空间

// 定义一台打印机
func printer(s string) {
	for _, ch := range s {
		fmt.Printf("%c", ch) // 屏幕:stdout
		time.Sleep(300 * time.Millisecond)
	}
}

// 定义两个人使用打印机
func person1() { // person 先执行。
	printer("hello")
	flagChan <- struct{}{} // 写入数据,解除阻塞
}
func person2() { // person 后执行
	printer("world")
	flagChan <- struct{}{} // 写入数据,解除阻塞
}

func main() {
	go person1()
	<-flagChan // 拿到数据之前,一直阻塞,所以会等到go程执行结束,直到写入
	
	go person2()
	<- flagChan
	
	fmt.Println("\n程序执行结束!!!")
}

上一篇:Redis基本数据类型(5)-----Zset


下一篇:【例6-23】批量数据处理综合应用(1)