Go语言中的goroutine

Go并发优势

Go语言最大的特点就是从语言层面支持并发,开发者不用担心并发的底层逻辑、内存管理,只需要编写好自己的业务逻辑即可。Go语言也提供了十分强大的垃圾回收机制,开发者不用担心创建的量如何销毁。

 

在其他语言中,编写并发程序往往需要使用其他的并发库才能实现。而在Go语言里,想要编写一个并发程序是非常容易的事情,它不需要额外引用其他的第三方库,只需要使用“go”关键字就可以实现。

 

在Go语言里,只需要使用"go"加上函数名称就可以让这个函数变为并发函数,如下示例:

package main

func run(arg string) {
    // 此线程的任务
}

func main() {
    go run("this is new thread")
}

 

Go语言的并发基于CSP(Communication Sequential Process,通信顺序进程)模型,CSP模型是用于描述两个独立的并发实体通过共享的通信管道(channel)进行通信的并发模型。

CSP中channel是一类对象,它不关注发送消息的实体,而关注发送消息时使用的通信管道。 简单来说,CSP模型提倡通过通信来共享内存,而非通过共享内存来通信。

基于CSP模型,Go语言通过通信的方式,通过安全的通道发送和接收数据以实现同步,避免了显式锁的问题,大大简化了并发编程的编写。

 

goroutine

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

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

 

goroutine定义

在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。

go 函数名(函数参数)

一旦我们使用了go关键字,函数的返回值就会被忽略,故不能使用函数返回值来与主线程进行数据交换,而只能使用channel。

 

线程和协程的区别

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

  1. 线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这样代价就小了。
  2. 协程的切换时间点是由调度器决定,而不是由系统内核决定的,尽管它们的切换点都是时间片超过一定阈值,或者是进入I/O或睡眠等状态时。
  3. 基于垃圾回收的考虑,Go实现了垃圾回收,但垃圾回收的必要条件是内存位于一致状态,因此就需要暂停所有的线程。如果交给系统去做,那么会暂停所有的线程使其一致。但对于Go语言来说,调度器知道什么时候内存位于一致状态,所以也就没有必要暂停所有运行的线程。

 

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

 

创建goroutine

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

我们只需要在函数调用语句前面添加go关键字,就可以创建并发执行单元。开发人员无须了解任何执行细节,调度器会自动将其安排到合适的系统线程上去执行。

go 函数名(参数名)

 

runtime包

Go语言中runtime(运行时)包实现了一个小型的任务调度器。这个调度器的工作原理和系统对线程的调度类似,Go语言调度器可以高效地将CPU资源分配给每一个任务。以下主要介绍三个函数:Gosched()Goexit()GOMAXPROCS()

 

Gosched()

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
	checkTimeouts()
	mcall(gosched_m)
}

Gosched()使当前Go协程放弃处理器,以让其他Go协程运行。它不会挂起当前Go协程,因此当前Go协程未来会恢复执行。

如下示例:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Println("go")
		}
	}()

	for i := 0; i < 2; i++ {
		runtime.Gosched()
		fmt.Println("main")
	}
}

运行结果如下:

在这里插入图片描述

Go语言的协程是抢占式调度的,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU §转让出去,让其他goroutine能被调度并执行。

一般出现如下几种情况,goroutine就会发生调度:

  1. syscall
  2. C函数调用(本质上和syscall一样)
  3. 主动调用runtime.Gosched
  4. 某个goroutine的调用时间超过100ms,并且这个goroutine调用了非内联的函数。

 

Goexit()

// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.
func Goexit()

Goexit()终止调用它的Go协程,但其他Go协程不会受影响。Goexit()会在终止该Go协程前执行所有defer的函数。

如下示例:

package main

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

func Task1() {
	defer fmt.Println("task1 stop")
	fmt.Println("task1 start")
	fmt.Println("task1 work")
}

func Task2() {
	defer fmt.Println("task2 stop")
	fmt.Println("task2 start")
	runtime.Goexit() // 效果和return一样
	fmt.Println("task2 work")
}

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

运行结果如下:
在这里插入图片描述

 

GOMAXPROCS()

GOMAXPROCS(n int)函数可以设置程序在运行中所使用的CPU数,Go语言程序默认会使用最大CPU数进行计算。

// GOMAXPROCS sets the maximum number of CPUs that can be executing
// simultaneously and returns the previous setting. It defaults to
// the value of runtime.NumCPU. If n < 1, it does not change the current setting.
// This call will go away when the scheduler improves.
func GOMAXPROCS(n int) int

GOMAXPROCS()设置可同时执行的最大CPU数,并返回先前的设置。若n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过NumCPU查询。

package main

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

func main() {
	n := runtime.GOMAXPROCS(1)
	fmt.Println("先前的CPU核数设置为: ", n)

	last := time.Now()
	for i := 0; i < 100000; i++ {
		go func() {
			// 耗时任务
			a := 999999 ^ 9999999
			a = a + 1
		}()
	}

	now := time.Now()
	fmt.Println(now.Sub(last))
}

运行结果如下:

在这里插入图片描述

当改为12核进行计算时,效率得到了显著的提升,运行结果如下:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值