Go语言并发学习

56 阅读4分钟

Go语言并发学习

1、并发介绍

进程与线程

A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发和并行

A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。

协程和线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。

2、goroutine

实例代码

package main

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

// 并行学习
// 每隔一秒输出hello, world!
// strconv.Itoa函数参数整型数字对应的数字字符串。
func test() {
	for i := 0; i <= 10; i++ {
		fmt.Println("Test Hello World!" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
func main() {
	go test() // 开启协程
	for i := 0; i <= 10; i++ {
		fmt.Println("Main Hello Golang!" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

运行结果

Main Hello Golang!0
Test Hello World!0
Test Hello World!1
Main Hello Golang!1
Main Hello Golang!2
Test Hello World!2
Test Hello World!3
Main Hello Golang!3
Test Hello World!4
Main Hello Golang!4
Test Hello World!5
Main Hello Golang!5
Main Hello Golang!6
Test Hello World!6
Main Hello Golang!7
Test Hello World!7
Test Hello World!8
Main Hello Golang!8
Main Hello Golang!9
Test Hello World!9
Test Hello World!10
Main Hello Golang!10

注意事项

  1. 如果主线程退出了,即使协程没有执行完毕,也会退出。
  2. 协程也有可能在主程序完成前就退出了

MPG模型

1、M: 操作系统的主线程
2、P: 协程执行需要的上下文
3、G: 协程

runtime包

1.1.1. runtime.Gosched()

让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
    // 主协程
    for i := 0; i < 2; i++ {
        // 切一下,再次分配任务
        runtime.Gosched()
        fmt.Println("hello")
    }
}

1.1.2. runtime.Goexit()

退出当前协程(一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            // 结束协程
            runtime.Goexit()
            defer fmt.Println("C.defer")
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}

1.1.3. runtime.GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

Go语言中的操作系统线程和goroutine的关系:

  • 1.一个操作系统线程对应用户态多个goroutine。
  • 2.go程序可以同时使用多个操作系统线程。
  • 3.goroutine和OS线程是多对多的关系,即m:n。

3、Channel

channel的本质是一个数据机构-队列

数据先进先出

线程安全,多gorountine访问时,不需要加锁,本身就是线程安全的

基本使用

var 变量名 chan 数据类型

package main

import "fmt"

func main() {
	// 演示一下管道的使用
	var intChan chan int
	intChan = make(chan int, 3)
	fmt.Printf("%v, %p", intChan, &intChan)
	fmt.Println()
	// 向管道写入数据,数据数量不能超过其容量
	intChan <- 10
	fmt.Println(len(intChan), cap(intChan))
}