Golang语言中最重要goroutine和channel,你真的入门了吗

1、goroutine

协程也叫轻量级线程,为什么说是一个轻量级的线程呢?协程可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常不能超过1万个。在Go语言提供所有系统调用操作,都会出让CPU给其他goroutine,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖CPU的核心数量。

goroutine是Go语言轻量级线程实现,由Go运行时(runtime)管理的。

栗子:假设我们需要实现一个函数sum(),它把两个参数相加,并且结果打印,代码如下:

func sum(x, y int) {
	z := x + y
	fmt.Println(z)
}

那么我们如何让这个函数并发执行呢?非常简单,代码如下:

go sum(1,1)

在一个函数调哦那个前面加上go关键字,这次调用就会在一个新的goroutine中并发执行。如果调用返回时,那么goroutine也自动结束了。(如该函数有返回值,那么这个返回值会被丢弃

回到之前sum函数的栗子,代码如下:

func sum(x, y int) {
	z := x + y
	fmt.Println(z)
}
func main() {
	for i := 0; i < 10; i++ {
		go sum(i,i)
	}
}

上面代码,我们使用for循环中调用10次sum()函数,它们是并发执行,但是发现运行后,控制台啥也没有输出?这是为什么呢?要解释这个现象,我们需要了解Go语言程序执行机制。

Go程序从初始化main package并执行main()函数开始,当main()函数返回时,程序退出, 且程序并不等待其他goroutine(非主goroutine)结束。

对于上面的例子,主函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执行sum(i, i)的goroutine没有来得及执行,所以程序没有任何输出。

相信大家问题找到了,那么如何解决?

相信大家想到使用sleep循环等待来所有线程执行完毕,但是Go语言有自己的推荐方式。我们要让主函数等待所有goroutine退出后再返回,但是不知道goroutine啥时候退出,这时需要了解goroutine之间通信问题。

栗子1:

package main

import "fmt"

func demo(count int) {
	for i := 0; i < 100; i++ {
		fmt.Println(count,":",i)
	}
}
func main() {
	for i := 0; i < 5; i++ {
		go demo(i)
	}
}

运行后,发现什么也不输出,为什么?解释这个现象是需要我们了解Go语言的程序执行机制。

首先Go程序从main方法执行完,就直接退出,它是不会等待goroutine结束,那么我们应该给它一个休眠等待也可以。

协程Coroutine:

  • 是一种轻量级“线程”
  • 最重要的是非抢占式多任务处理,有协程主动交出控制权。(非抢占就是正在执行的不允许中断)
  • 编译器/解析器/虚拟机层面的多任务
  • 多个协程肯在一个多个线程上运行
func main() {
	var a [10]int
	for i := 0; i < 10; i++ {
		go func(i int) {//匿名函数
			for {
				a[i]++
				runtime.Gosched()
			}
		}(i)
	}
	time.Sleep(time.Millisecond)
	fmt.Println(a)
}

goroutine可能的切换点

  • I/O,select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()
  • 只是参考,不能保证切换,不能保证在其他地方不切换

面试题:

  1. Go协程,Goroutine阻塞
  2. Go map底层,扩容机制
  3. new 和 make的区别
  4. 有缓存和无缓存channel的区别
  5. 2个协程交替打印字母和数字

2、WaitGroup简介

Go中sync包提供基本同步基元,如互斥锁等,除了Once和WaitGroup类型,大部分只适用于低水平程序线程,高水平同步线程使用channel通信会更好。

WaitGroup翻译为等待组,其实就是计数器,只要计数器中有内容将一直阻塞。

Go语言标准库中WaitGroup只有三个方法

  • Add(delta int)表示向内部计数器添加增量(delta)其中参数delta可以是负数
  • Done()表示减少WaitGroup计数器的值,应当在程序最后执行,相当于Add(-1)
  • Wait()表示阻塞直到WaitGroup计数器为

栗子:互斥锁使用

func demo(count int) {
	for i := 0; i < 10; i++ {
		fmt.Println(count,":",i)
	}
}
func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go demo(i)
		wg.Done()
	}
  //time.Sleep(time.Millisecond)//不使用
	wg.Wait()
	fmt.Println("ok")
}

3、互斥锁和读写锁

在Go语言中,当多个协程操作一个变量时可能会出现冲突问题,也许会导致程序出异常也许不会,我们可以使用go run -race查看是否有竞争。

任何一个新问题出现,我们会想如何解决?

使用sync.Mutex对内容加锁

栗子:互斥锁使用

var (
	num = 100
	wg sync.WaitGroup
	m sync.Mutex
)
func demo1() {
	m.Lock()
	for i := 0; i < 10; i++ {
		num = num - i
	}
	m.Unlock()
	wg.Done()
}
func main() {
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go demo1()
	}
	wg.Wait()
	fmt.Println(num)
	fmt.Println("ok")
}

读写锁(RWMutex)

先看Go语言标准库中的API

type{
  func (rw *RWMutex) Lock() //禁止其他协程读
  func (rw *RWMutex) Unlock()
  func (rw *RWMutex) RLock() //禁止其他协程写入,只能读取
  func (rw *RWMutex) RUnlock() 
  func (rw *RWMutex) RLocker()
}

互斥锁的锁事同一时间只能一个goroutine运行,而读写锁表示在锁范围内数据的读写操作

栗子:map在并发下读写结合读写锁完成

func main() {
	var rwm sync.RWMutex
	var wg sync.WaitGroup
	wg.Add(10)
	m := make(map[int]int)
	for i := 0; i < 10; i++ {
		go func(j int) {
			rwm.Lock()
			m[j] = j
			fmt.Println(m)
			rwm.Unlock()
			wg.Done()
		}(i)
	}
	wg.Wait()
	fmt.Println("ok")
}

4、channel

先了解并发通信概念

在工程上,有两种最常见的并发通信模型:共享数据和消息

上述例子,我们可以使用加锁处理,但是,实现一个如此简单的功能,却写出如此臃肿而且难以理解的代码,显然不符合GO语言风格,那么加锁方式这里就不演示,直接介绍Go语言提供的goroutine间通信方式channel

channel是Go提供goroutine间的通信方式,使用channel可以使多个goroutine之间通信。channel是进程内的通信方式,通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如需跨进程通信,Go建议用分布式系统的方法来解决,如使用Socket或者HTTP等通信协议,Go语言在网络方面也有非常完善的支持。

channel是类型相关的,一个channel只能传递一种类型的值,这个类型需要在声 明channel时指定。接下来,了解channel语法,简单一个例子,代码如下:

package main

import "fmt"

func Count(ch chan int) {
	ch <- 1
	fmt.Println("Counting")
}
func main() {
	chs := make([]chan int,10) //定义10个channel的数组
	for i := 0; i < 10; i++ { //数组中的每个channel分配给10个不同的goroutine
		chs[i] = make(chan int)
		go Count(chs[i])
	}
	for _,ch := range(chs) {
		<-ch
	}
}

channel基本语法

var channame chan ElementType
var channame chan <- ElementType //只写
var channame <- chan ElementType //只读
chanName := make(chan int) //无缓存channel
chanName := make(chan in,0) //无缓存channel
chanName := make(chan int,100) //有缓存channel

栗子说明:

ch <- 1 //向ch添加一个值为1
<- ch //从ch取出一个值
a := <- ch //从ch取出一个值并赋值给a
a,b := <- ch //从ch取出一个值赋值给a,如果ch已经关闭或ch没有值,b为false

注:如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel 中被写入数据为止,我们也可以控制channel只读或写,后面讲解。

栗子1:同步,主协程和子协程之间通信

func main(){
    ch := make(chan int)
    go func() {
        ch <- 996 //向ch添加元素
    }()
    a := <- ch
    fmt.Println(a)
    fmt.Println("程序结束!")
}

栗子2:两个子协程的通信

使用channel实现两个goroutine之间通信。(go语言中没有使用共享内存完成线程通信,而是使用channel)代码如下:

func two() {
    tc := make(chan string)
    ch := make(chan int)
    // 第一个协程
    go func() {
        tc <- "协程A,我在添加数据"
        ch <- 1
    }()
    // 第二个协程
    go func() {
        content := <- tc
        fmt.Printf("协程B,我在读取数据:%s\n",content)
        ch <- 2
    }()
    <- ch
    <- ch
    fmt.Println("程序结素!")
}
func main(){
    two()
}

注:我们在往出取数据时,都是箭头,数据多的时候很麻烦呀,怎么办?使用for range获取channel中的内容。

func zs() {
    ch1 := make(chan string)
    ch2 := make(chan int)
    go func() {
        for i := 97; i <= 97+26; i++ {
            // ch1 <- strconv.Itoa(i)
            // 把int的值通过ASCII转换成字符串
            ch1 <- fmt.Sprintf("%c",i)
        }
        ch2 <- 1
    }()
    go func() {
        for n := range ch1{
            fmt.Println(n)
        }
        ch2 <- 2
    }()
    <- ch2
}

go出现deadlck死锁情况,如:

func main() {
	ch := make(chan int,3)
	ch <- 1
	//<- ch
	ch <- 1
	ch <- 1
	ch <- 1
	fmt.Println("ok")
}

想channel添加数据超过缓存,会出现死锁。

使用Select来进行调度

Select 和 swath结构很像,但是select中的case的条件只能是I/O。

func main() {
	ch1 := make(chan int,1)
	ch2 := make(chan string,1)
	ch1 <- 1
	ch2 <- "Hello world"
	select {
	case a1 := <- ch1:
		fmt.Println(a1)
	case a2 := <- ch2:
		fmt.Println(a2)
	default:
		fmt.Println("default")
	}
}

select里面case是随机执行的,如果都不满足条件,那么久执行default

栗子:实现一个一直接收消息

func main() {
	ch := make(chan int)
	for i := 1; i <= 10; i++ {
		go func(j int) {
			ch <- j
		}(i)
	}
	for {
		select {
		case a1 := <- ch:
			fmt.Println(a1)
		default:
		}
	}
}

select总结:

  • 每个case必须是一个I/O操作
  • case是随机执行的
  • 如果所有case不能执行,那么会执行default
  • 如果所有case不能执行,且没有default,会出现阻塞

5、Go语言中的GC

Go语言采用stop The World方式,早起版本1.5之前垃圾回收效率不算高,在1.5开始支持并发收集,到1.8版本,Go能把STW时间优化到100微秒级。

怎么时候Go会触发GC?

  • 当需要申请内存时,发现GC是上次GC两倍时会触发
  • 每2分钟自动运行一次GC

Go语言常用GC算法:

  • 三色标记法,是mark And Sweep改进版,将逻辑上分为白色区(未搜索),灰色区(正搜索),黑色区(已搜索)

GC调优:

  • 少对象复用,局部变量尽量少声明,多个小对象可以放入到结构体中,这样方便GC扫描
  • 少用string的"+"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值