Go学习(八):并发编程1

目录

1、概念:

进程和线程

并发和并行

协程和线程

2、goroutine

2.1 使用goroutine

2.2 可增长的栈

2.3 goroutine调度

3、runtime包Golang进程权限调度包runtimehttps://www.cnblogs.com/wt645631686/p/9656046.html

runtime.Gosched():让出CPU时间片,重新等待安排任务

runtime.Goexit():终止携程

runtime.GOMAXPROCS(),需要使用多少个OS线程来同时执行Go代码

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

4、Channel:函数与函数间交换数据

4.1 channel的基本使用:初始化,三种操作

4.2 两种方法在接收值的时候判断通道是否被关闭

4.3 单向通道(限制只能收or发)

 4.4 通道异常总结

 5、Goroutine池

6、定时器

1、概念:

进程和线程
A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
并发和并行
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。
协程和线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。

2、goroutine

goroutine 高并发,奉行通过通信来共享内存,而不是共享内存来通信。

goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

2.1 使用goroutine

package main

import (
	"fmt"
	"sync"
	"time"
)

func hello() {
	fmt.Println("Hello Goroutine!")
}

// 启动多个goroutine
// 使用了sync.WaitGroup来实现goroutine的同步
var wg sync.WaitGroup

func hellosync(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}

func main() {
	go hello()  启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	//只会打印main goroutine done!
	//因为当main()函数结束时,goroutine就一同结束了,还来不及执行打印
	time.Sleep(time.Second)
	// main goroutine done!
	// Hello Goroutine!
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hellosync(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束

}

主携程退出,则其他任务不执行

package main

import (
    "fmt"
    "time"
)

func main() {
    // 合起来写
    go func() {
        i := 0
        for {
            i++
            fmt.Printf("new goroutine: i = %d\n", i)
            time.Sleep(time.Second)
        }
    }()
    i := 0
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(time.Second)
        if i == 2 {
            break
        }
    }
}
1.1.1. goroutin
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2

2.2 可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

2.3 goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • 1.G很好理解,就是goroutine,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • 2.Processor管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。P的个数是通过runtime.GOMAXPROCS设定(最大256)
  • 3.M(machine)(thread)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

 goroutine的优势:

  • Go运行时(runtime)自己的调度器调度的
  • goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,充分利用多核优势

3、runtime包Golang进程权限调度包runtimeicon-default.png?t=N7T8https://www.cnblogs.com/wt645631686/p/9656046.html

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")
    }
}

//world
//world
//hello
//hello

runtime.Goexit():终止携程

调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。

终止当前goroutine前会先执行此goroutine的还未执行的defer语句

不能再主函数调用runtime.Goexit,因为会引发panic

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 {
    }
}
//输出
//B.defer
//A.defer

runtime.GOMAXPROCS(),需要使用多少个OS线程来同时执行Go代码

默认值是机器上的CPU核心数,用来设置可以并行计算的CPU核数最大值,并返回之前的值。

package main

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

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() {
	num := runtime.GOMAXPROCS(1)
	go a()
	go b()
	fmt.Println(num)  //输出了之前的值:8
	time.Sleep(time.Second)
}

//做完一个任务再做另一个任务,而且是B先,先进后出?
8
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

两个逻辑核心

package main

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

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)
}
B: 1
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 2
B: 3
B: 4
B: 5
B: 6
Go语言中的操作系统线程和goroutine的关系:
  • 1.一个操作系统线程对应用户态多个goroutine。
  • 2.go程序可以同时使用多个操作系统线程。
  • 3.goroutine和OS线程是多对多的关系,即m:n。

4、Channel:函数与函数间交换数据

问题:可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制

channel遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

4.1 channel的基本使用:初始化,三种操作

package main

import (
	"fmt"
	"sync"
)

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
	close(c)
	fmt.Println("通道c关闭成功")
}
func main() {
	//1、创建channel
	var ch chan int
	//通道是引用类型,通道类型的空值是nil
	fmt.Println(ch) // <nil> 声明的通道后需要使用make函数初始化之后才能使用。
	//make函数初始化
	ch1 := make(chan int)
	fmt.Println(ch1) //0xc000060060
	go recv(ch1)     // 启用goroutine从通道接收值
	//2、三种操作
	//2.1 发送(send)
	ch1 <- 10 // 把10发送到ch中,能编译,但执行会出现deadlock错误
	//因为是无缓冲通道,必须要有人接收,会在这里死锁
	//解决办法1:启用一个goroutine去接收值,recv(ch)启动要在发送数据前,不然还是错

	fmt.Println("解决办法1发送成功")

	//解决办法2:使用 有缓冲通道
	ch2 := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	//可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,但少用
	ch2 <- 10
	fmt.Println("解决办法2发送成功")
	//2.2 接收(receive),ret := <-c
	//2.3 关闭(close) close(c)
	//如果你的管道不往里存值或者取值的时候一定记得关闭管道
	//close(ch1)	//报错了?why是因为接收方已经关闭了吗
	//fmt.Println("ch1关闭成功")
	close(ch2)
	fmt.Println("ch2关闭成功")
}
<nil>
0xc000060060
接收成功 10
通道c关闭成功
解决办法1发送成功
解决办法2发送成功
ch2关闭成功

4.2 两种方法在接收值的时候判断通道是否被关闭

package main

import (
	"fmt"
)

// channel 练习
// 有两种方式在接收值的时候判断通道是否被关闭,常用for range
func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	// 开启goroutine将0~50的数发送到ch1中
	go func() {
		for i := 0; i < 50; i++ {
			ch1 <- i
		}
		close(ch1)
	}()
	// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
	go func() {
		for {
			i, ok := <-ch1 // 方法1:通道关闭后再取值ok=false
			if !ok {
				break
			}
			ch2 <- i * i
		}
		close(ch2)
	}()
	// 在主goroutine中从ch2中接收值打印
	for i := range ch2 { // 方法2:通道关闭后会退出for range循环
		fmt.Println(i)
	}
}

4.3 单向通道(限制只能收or发)

1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。
3.双向通道可以转为单向通道,反之不行
func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}

 4.4 通道异常总结

 5、Goroutine池

  • 本质上是生产者消费者模型
  • 可以有效控制goroutine数量,防止暴涨

例题:(还是有点不太明白为什么会deadlock)

  • 计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
  • 随机生成数字进行计算
package main

import (
	"fmt"
	"math/rand"
	"sync"
)

type Job struct {
	Id      int
	RandNum int
}
type Result struct {
	//这里必须传对象实例??
	job *Job
	sum int
}

// 使用等待组来等待所有协程完成
var wg sync.WaitGroup
var resultWg sync.WaitGroup

// 创建工作池
// 参数1:开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
	for i := 0; i < num; i++ {
		wg.Add(1)
		go func(jobChan chan *Job, resultChan chan *Result) {
			defer wg.Done()
			//获取job管道的数据
			for job := range jobChan {
				r_num := job.RandNum
				//执行加操作
				var sum int
				for r_num != 0 {
					tmp := r_num % 10
					sum += tmp
					r_num /= 10
				}
				//得到结果
				r := &Result{job, sum}
				//将结果传入管道
				resultChan <- r
			}
		}(jobChan, resultChan)
	}
}

func main() {
	//生成随机数管道,需要记录job的id
	jobChan := make(chan *Job, 128) //有缓冲通道

	//计算结果管道
	resultChan := make(chan *Result, 128)

	//生成随机数,输入到管道
	for id := 0; id < 100; id++ {
		job := &Job{id, rand.Int()}
		jobChan <- job

	}

	//创建工作池
	createPool(16, jobChan, resultChan)

	// 关闭jobChan,告诉协程不会再有新的工作,没有这个会报All goroutines are asleep, deadlock
	close(jobChan)

	//打印协程
	resultWg.Add(1)
	go func(resultChan chan *Result) {
		defer resultWg.Done() // 打印协程完成后减少等待组计数

		for result := range resultChan {
			fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
				result.job.RandNum, result.sum)
		}

	}(resultChan) //匿名函数传参

	// 等待所有工作协程完成
	wg.Wait()

	// 关闭 resultChan,告诉打印协程不再需要结果
	close(resultChan)

	// 等待打印协程完成
	resultWg.Wait()
}

如果你不关闭 jobChan,那么工作协程将一直等待从 jobChan 中接收任务,而主程序中也没有向 jobChan 中发送更多的任务,这将导致工作协程一直等待下去,最终出现死锁。因此,在使用有缓冲通道时,需要在合适的时机关闭通道以告知接收方没有更多的数据。

6、定时器

原理:传参是告诉 Timer 需要等待多长时间,Timer 是带有一个缓冲的 channal,在定时时间到达之前,没有数据写入 Timer.C,读取操作会阻塞当前协程,到达定时时间时,会向 channel 写入数据(当前时间),阻塞解除,被阻塞的协程得以恢复运行,达到延时或者定时执行的目的。Golang 定时器使用方法汇总-CSDN博客

type Timer struct {
   C <-chan Time
   r runtimeTimer
}

6.1 Timer基本使用

package main

import (
	"fmt"
	"time"
)

//定时器使用

func main() {
	//1.timer基本使用

	t1 := time.Now()
	fmt.Println("now time1: ", t1.Format("2006-01-02 15:04:05"))
	t2 := time.NewTimer(2 * time.Second)
	t3 := <-t2.C  超时时间还没到时,协程会阻塞;超时时间到了之后会返回当前时间
	fmt.Println("now time2: ", time.Now().Format("2006-01-02 15:04:05"))
	fmt.Println("now time3: ", t3.Format("2006-01-02 15:04:05"))
	// now time1:  2023-11-01 21:50:04
	// now time2:  2023-11-01 21:50:06
	// now time3:  2023-11-01 21:50:06

	//2、timer.After
	//After() 函数接受一个时长 d,然后 After() 等待 d 时长,等待时间到后,将等待完成时所处时间点写入到 channel 中并返回这个只读 channel。
	//NewTimer(d).C 和after 每次都是 return 了一个新的对象,已踩坑
	t4 := time.After(time.Second * 2)
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
	time.Sleep(time.Second * 4)
	chant := <-t4
	fmt.Println("now time: ", chant.Format("2006-01-02 15:04:05"))
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
	// now time:  2023-11-01 21:42:08
	// now time:  2023-11-01 21:42:10
	// now time:  2023-11-01 21:42:12

	//3、time.Reset()
	//<-t2.C	使用会报错,即timer只能响应1次,想要重新使用就需要reset
	//如果调用 time.Reset() 和 time.Stop() 时,timer 已过期或者已停止,则会返回 false
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
	t5 := time.NewTimer(4 * time.Second)
	//ok := t2.Reset(2 * time.Second) //返回false
	ok := t5.Reset(2 * time.Second)
	fmt.Println("ok: ", ok)
	<-t5.C
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
	// now time:  2023-11-01 21:52:09
	// ok:  true
	// now time:  2023-11-01 21:52:11

	//4、time.Stop()
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))
	t6 := time.NewTimer(4 * time.Second)
	t6.Stop() //停止定时器
	fmt.Println("now time: ", time.Now().Format("2006-01-02 15:04:05"))

}

6.2 Ticker

Ticker 是一个周期触发定时的定时器,按给定时间间隔往 channel 发送系统当前时间,而 channel 的接收者可以以固定的时间间隔从 channel 中读取

package main
import (
	"fmt"
	"time"
)
func main() {
	for {
		select {
		case <-time.Tick(2 * time.Second):
			fmt.Println("2 second over:", time.Now().Second())
		case <-time.After(7 * time.Second):
			fmt.Println("5 second over, timeover", time.Now().Second())
			return
		}
	}
}
start second: 14
1 second over: 15
1 second over: 16
1 second over: 17
1 second over: 18
1 second over: 19
1 second over: 20
1 second over: 21
7 second over: 21

package main

import (
    "fmt"
    "time"
)

func main() {
    //创建定时器,每隔1秒后,定时器就会给channel发送一个事件(当前时间)
    ticker := time.NewTicker(time.Second * 1)

    i := 0
    go func() {
        for { //循环
            <-ticker.C
            i++
            fmt.Println("i = ", i)

            if i == 5 {
                ticker.Stop() //停止定时器
            }
        }
    }() //别忘了()

    //死循环,特地不让main goroutine结束
    for {
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值