go语言学习-并发概念以及goroutine

时间:2022-01-23 01:09:17

进程和线程

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

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

携程和线程

  • 协程,独立的栈空间,共享堆空间,调度由用户自己控制,
  • 线程,一个线程上可以跑多个协程,协程是轻量级的线程

Goroutine

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

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了。

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

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

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束.

启动goroutine示例

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

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second) //休眠一秒等待
} 

并发的clock服务

顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端

package main

//TCP服务的时钟,持续的写入时间

import (
	"io"
	"log"
	"net"
	"time"
)

func HandConn(c net.Conn) {
	//处理完整客户端连接
	defer c.Close() //延迟关闭
	//for死循环持续执行,直到写入失败
	for {
		_, err := io.WriteString(c, time.Now().Format("15:04:04\n")) //以当前H:m:s格式写入io
		if err != nil {
			return //断开写入
		}
		time.Sleep(1 * time.Second) //休眠一秒
	}

}

func main() {
	//监听本机TCP的8000端口
	//listener对象的accept方法会直接阻塞,直到一个新的连接被创建
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err) //记录错误并退出
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go HandConn(conn) //开启并发,能够等待多个客户端连接而不会被阻塞
	}
}

使用netcat工具来进行连接,多个终端连接并不会导致阻塞

go语言学习-并发概念以及goroutine

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel的类型和创建

chanel是一种类型,一种引用类型

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道 

//通道是引用类型,默认是nil空值
//声明通道后需要make初始化后才能使用
//make(chan 元素类型, [缓冲大小])  
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int) 

channel的操作

//发送
ch :=make(chan int)
ch <-10 //把10发送到ch中

//接收
x:=<-ch //从ch中接收值并赋值给变量x
<-ch    //从ch中接收值,忽略结果

//关闭
close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

 1.对一个关闭的通道再发送值就会导致panic。

   2.对一个关闭的通道进行接收会一直获取值直到通道为空。

   3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。

   4.关闭一个已经关闭的通道会导致panic。 

无缓冲的通道

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

go语言学习-并发概念以及goroutine

有缓冲的通道

go语言学习-并发概念以及goroutine

又称异步通道,解决阻塞问题

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}   

单向通道

将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

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

 1.chan<- int是一个只能发送的通道,可以发送但是不能接收;

   2.<-chan int是一个只能接收的通道,可以接收但是不能发送。 

通道的异常总结

go语言学习-并发概念以及goroutine


gorouline池

  • 本质上是生产者消费者模型
  • 可以有效控制goroutine数量,防止暴涨
  • 需求:
  • 计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
  • 随机生成数字进行计
package main

import (
	"fmt"
	"math/rand"
)

type Job struct {
	// id
	Id int
	// 需要计算的随机数
	RandNum int
}

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

// 创建工作池
// 参数1:开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
	// 根据开协程个数,去跑运行
	for i := 0; i < num; i++ {
		go func(jobChan chan *Job, resultChan chan *Result) {
			// 执行运算
			// 遍历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
				}
				// 想要的结果是Result
				r := &Result{
					job: job,
					sum: sum,
				}
				//运算结果扔到管道
				resultChan <- r
			}
		}(jobChan, resultChan)
	}
}

func main() {
	// 需要2个管道
	// 1.job管道
	jobChan := make(chan *Job, 128)
	// 2.结果管道
	resultChan := make(chan *Result, 128)
	// 3.创建工作池
	createPool(64, jobChan, resultChan)
	// 4.开个打印的协程
	go func(resultChan chan *Result) {
		// 遍历结果管道打印
		for result := range resultChan {
			fmt.Printf("job id:%v randnum:%v result:%d\n", result.job.Id,
				result.job.RandNum, result.sum)
		}
	}(resultChan)
	var id int
	// 循环创建job,输入到管道
	for {
		id++
		// 生成随机数
		r_num := rand.Int()
		job := &Job{
			Id:      id,
			RandNum: r_num,
		}
		jobChan <- job
	}
}