Goroutine和Channel

线程和进程基本介绍

  1. 进程就是程序程序在操作系统中的次执行过程,是系统进行资源分配和调度的基本单位
  2. 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
  3. 一个进程可以创建核销毁多个线程,同一个进程中的多个线程可以并发执行
  4. 一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

Go协程

Go主线程

Go主线程:一个Go线程上,可以起多个协程,协程是轻量级的线程

Go协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

快速入门

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 world" + strconv.Itoa(i))
		time.Sleep(time.Second)

	}
}

快速入门总结

  1. 开启协程的关键字时go
  2. 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常消耗cpu资源
  3. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  4. Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其他编程语言的并发机制是一般基于线程的,开启过多的线程,资源消耗大,这里就凸显Golang在并发的优势了

goroutine的调度模型

详见

Golang运行的cpu数

func main() {

	num := runtime.NumCPU() 	// 获取当前系统的cpu的数量

	runtime.GOMAXPROCS(num)		// 运行go程序的核数 go1.8后不需要设置,1.8以前需要设置
	fmt.Println("num = ", num)
}

让出时间片

我们可以在每个goroutine中控制何时主动出让时间片给其他goroutine,这可以使用runtime包中的Gosched()函数实现。
实际上,如果要比较精细地控制goroutine的行为,就必须比较深入地了解Go语言开发包中runtime包所提供的具体功能。

Channel

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel有类型的,一个string的channel只能放string类型的数据

声明与创建

声明语法:var 变量名 chan 数据类型

var intChan chan int // 用于存放int数据
var mapChan chan map[int]string // 存放 map[int]string 类型
var perChan chan Person
var perChan2 chan *Person

创建:c := make(chan type, cap),最多可以放cap数量的数,如果不带cap,则容量为1

  1. channel是引用类型
  2. channel必须初始化才能写入数据,即make后才能使用
  3. 管道是由类型的
var intChan chan int
intChan = make(chan int, 3) 

fmt.Printf("intChan 的值 = %v intChan 本身的地址 = %p 
", intChan, &intChan)

// 写入数据,不使用协程,如果channel已满,继续写入会出现 dead loack
num := 211
intChan <- 10
intChan <- 50
intChan <- num
intChan <- 100

fmt.Printf("channel len = %v cap = %v 
", len(intChan), cap(intChan))

// 读取数据,不使用协程,如果channel已空,继续读出会出现 dead loack
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
fmt.Println(num1, num2, num3)

注意事项

  1. channel中只能存放指定的数据类型
  2. channel的数据放满后,就不能再放入了
  3. 如果从channel取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock

channel的遍历和关闭

  1. channel的关闭
    使用内置函数close可以关闭channel,当channel关闭后,就不能再想channel写数据了,但是任然可以从该channel读取数据
intChan := make(chan int, 3)
intChan <- 100
intChan <- 200
close(intChan)

// panic: send on closed channel
// intChan <- 300 

v := <-intChan
fmt.Println(v)

当channel关闭且数据都读完了,再读数据会读到该数据类型的零值,且第二个返回值为false

intChan := make(chan int, 3)

intChan <- 1
intChan <- 2
intChan <- 3

var v int
var ok bool

v, ok = <-intChan
fmt.Println(v, ok)

// close(intChan)
v, ok = <-intChan
fmt.Println(v, ok)

v, ok = <-intChan
fmt.Println(v, ok)

v, ok = <-intChan // 输出0, false
fmt.Println(v, ok)
  1. channel的遍历
    channel支持for-range的方式进行遍历,请注意两个细节
  • 在遍历时,如果channel没有关闭,则会出现deadlock的错误
  • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会�遍历
intChan := make(chan int, 10)

for i := 1; i <= 10; i++ {
	intChan <- i
}

close(intChan)

for v := range intChan {
	fmt.Println(v)
}

Channel使用细节和注意事项

  1. channel可以声明为只读,或者只写的性质

单向channel只能用于发送或者接收数据。 channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。

在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制 该函数中可以对此 channel的操作, 比如只能往这个 channel写,或者只能从这个channel读。

从设计的角度考虑,所有的代码应该都遵循“最小权限原则”,从而避免没必要地使用泛滥问题,进而导致程序失控。单向channel也是起到这样的一种契约作用。

// 1. 在默认情况下,管道时双向的
var chan1 chan int // 可读可写

// 2. 声明为只写
var chan2 chan<- int

// 3. 声明为只读
var chan3 <-chan int
  1. 使用Select
    select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
select {
	case <-chan1:
	// 如果chan1成功读到数据,则进行该case处理语句
	case chan2 <- 1:
	// 如果成功向chan2写入数据,则进行该case处理语句
	default:
	// 如果上面都没有成功,则进入default处理流程
}

可以看出, select不像switch,后面并不带判断条件,而是直接去查看case语句。每个case语句都必须是一个面向channel的操作。

  1. goroutine中使用recover,解决协程中出现的panic,导致程序崩溃的问题

同步

Go语言包中的sync包提供了两种锁类型: sync.Mutex和sync.RWMutex。
Mutex是最简单的一种锁类型,同时也比较暴力,当一个goroutine获得了Mutex后,其他goroutine就只能乖乖等到这个goroutine释放该Mutex。
RWMutex相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用RLock()方法;而写锁(调用Lock()方法)会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占。

全局唯一性操作

对于从全局的角度只需要运行一次的代码,比如全局初始化操作, Go语言提供了一个Once类型来保证全局的唯一性操作,具体代码如下:

var a string
var once sync.Once
func setup() {
	a = "hello, world"
}
func doprint() {
	once.Do(setup)
	print(a)
}
func twoprint() {
	go doprint()
	go doprint()
}

原文地址:https://www.cnblogs.com/lxlhelloworld/p/14286062.html