go并发

最近在写go的多并法操作,写个笔记


前言

一、进程 VS 线程 VS 协程 VS goroutine

简单的个人理解(如有不正,望指出):

进程(Prosess):就是系统分配资源的最小单位,运行一个程序,操作系统将程序二进制文件从磁盘或其他地方加载进内存,并分配资源(linux系统为例,就是申请进程描述符/进程堆栈/用户空间/内核栈等)来创建一个进程。
通俗理解,进程就是创建一个可以跳舞的平台,程序就是剧本。

线程(Thread):就是CPU调度的最小单位,进程内部具体的执行流程,与进程的其他线程贡献这个进程的内存空间。以为linux为例,线程也叫做轻量级进程,与其他同一个进程的线程共享进程的空间,注意这是从内核层面来看的。
通俗理解就是派多个小丑上台表演。


1.1、多进程 或者 多进程的弊端

    以JAVA的JVM为例,JVM的多线程模型是与内核线程呈1:1的关系的,也就是你在JAVA代码start多少个线程,OS就帮你申请多少个内核线程。弊端显而易见,同样以Linux为例子:

  1. 问题一:每个用户可以创建的进程/线程数量有限的。当然你可以设置这个上限阈值,怎么设置这里不说。大量线程的创建会达到当前用户可以创建的线程上限,也就是不能永久地创建。
  2. 问题二:大量进程的运行会导致频繁的上下文切换。假设CPU核心只有一个,线程有100个,切换耗时1ms,现在要求在1s内所有线程都所要得到响应,那这一秒内花在切换上的时间是多少我不算了,太蛋疼了。

通俗理解:一群奇葩的观众花了这个多钱买了票,肯定要求在这个固定的时间内看尽量多的节目,那怎么办?只好叫几个小丑(几个CPU核心)演出几分钟,然后叫他们下台(上下文切换),换其他小丑演另一个节目,然后又是演了几分钟,下台,继续换,观众不好受,导演(CPU)更不好受。


1.2、协程 与其弊端

    总结之前的问题就是,线程都是贪婪的,过多的线程会导致大多数时间都花在上下文切换上了,同一时间段内真正花在运算上的太少了。

    一个解决办法就是后来提出的协程

协程(coroutine):协程就是可以看作从用户层面看的线程,也就是俗称的“用户线程“, 上面所说的线程就是由内核实现的,这个就是由用户自己实现的。可以这样看,某个线程P的程序自己实现了一套类似操作系统的调度策略,然后由自己来实现切换/调度/保存上下文的操作。也就是说内核看到的永远只有线程P,其内部有那些个协程不吉岛。也就是说如果这个线程P内有个协程C1是读IO的,一调用系统调用read,整个线程都被阻塞了(哭),其内部其他的CPU密集性协程就会被执行。
通俗理解,小丑还是那个小丑(线程),每天要演不同的剧本还要照顾MF的女朋友,突然某天小丑的女朋友生日了要请假(IO密集协程),只好被老板(OS)给阻塞了,等到过完生日了(IO读取完成),才能继续搬砖出演,继续被另一个老板压榨。


1.4、goroutine

    传统意义上的协程与内核线程的比例关系是N:1的关系,从这个比例可以看出只要有一个协程挂,这个线程也就挂了。
    go针对这个问题,在语言层面又封装了一个好东西——goroutine
    goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而goroutine支持并发的,goroutine与内核的关系可以看成N:M(多对多,且一般M>N)。因此Goroutine可以理解为一种Go语言的协程,但它又不是传统意义上的协程。同一时刻多个goroutine可以运行在一个或多个内核线程上。


1.5、goroutine实现原理

Go goroutine理解


二、goroutine的使用与并法

2.1 基本使用

Talk is cheap. Show me the code                     ——Linus Torvalds

2.1.1 匿名函数(无参)

func main()  {
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		wg.Done()
		fmt.Println("I have a little donkey, I never ride it ")
	}()
	wg.Wait()
}

2.1.2 匿名函数(带参)

func main()  {
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func(animal string) {
		wg.Done()
		fmt.Printf("I have a little %v, I never ride it \n", animal)
	}("donkey")
	wg.Wait()
}

2.1.2 实名函数(无参)

var wg = sync.WaitGroup{}
func say()  {
	go func() {
		wg.Done()
		fmt.Println("I have a little donkey, I never ride it ")
	}()
}
func main()  {
	wg.Add(1)
	go say()
	wg.Wait()
}

2.1.2 实名函数(带参)

var wg = sync.WaitGroup{}
func say(animal string)  {
	go func() {
		wg.Done()
		fmt.Printf("I have a little %v, I never ride it \n", animal)
	}()
}

func main()  {
	wg.Add(1)
	go say("donkey")
	wg.Wait()
}

3 goroutine并发(未完持续)

3.1 互斥锁(mutex)

package main
import (
    "fmt"
    "sync"
)
func main() {
    var mu sync.Mutex
    mu.Lock()
    go func() {
        fmt.Println("I have a little donkey, I never ride it")
        mu.Unlock()
    }()
    mu.Lock()
}

3.2 管道(channel)…CSP并发模型

等一个

package main
import (
    "fmt"
)
func main() {
    done := make(chan int, 1) // 带缓存通道
    //done := make(chan int) // 不带缓存通道, 可以兼容两种
    go func() {
        fmt.Println("I have a little donkey, I never ride it")
        done <- 1
    }()
    <-done
}

等多个

package main
import (
    "fmt"
)
func main() {
    done := make(chan int, 10) // 带10个缓存
    // 开N个后台打印线程
    for i := 0; i < cap(done); i++ {
        go func() {
            fmt.Println("I have a little donkey, I never ride it")
            done <- 1
        }()
    }
    // 等待N个后台线程完成
    for i := 0; i < cap(done); i++ {
        <-done
    }
}

3.3 WaitGroup

package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    // 开N个后台打印线程
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            fmt.Println("I have a little donkey, I never ride it")
            wg.Done()
        }()
    }
    // 等待N个后台线程完成
    wg.Wait()
}

3.4 设置CPU核心数

    在go跑goroutine的时候,初学者包括我自己都会发现,无论申请几个都不会把CPU承包(确实这个好处就是不像当时学JAVA多线程一样,开了20个线程后就要重启了,1:1模型的弊端之处)。
    对于这个,官方给出的答案是,这是当前版本的 Go 编译器还不能很智能地去发现和利用多核的优势。虽然我们确实创建了多个 goroutine,并且从运行状态看这些 goroutine 也都在并行运行,但实际上所有这些 goroutine 都运行在同一个 CPU 核心上,在一个 goroutine 得到时间片执行的时候,其他 goroutine 都会处于等待状态。
    从这一点可以看出,虽然 goroutine 简化了我们写并行代码的过程,但实际上整体运行效率并不真正高于单线程程序(因为比如只能用一个线程,一个核的CPU最多就100%不变了,并且还要把时间花在上下文切换上面,得不偿失…)。

解决方法:

  1. 设置go环境变量GOMAXPROCS
  2. 在程序中用runtime.GOMAXPROCS设置使用核心数,代码示例:
func main() {
	cpuNum := runtime.NumCPU() //获得当前设备的cpu核心数
	fmt.Println("cpu核心数:", cpuNum)
	runtime.GOMAXPROCS(cpuNum) //设置需要用到的cpu数量
}

3.5 web服务器的做法

main线程用select空等

func main() {
	go func() {
		for{
			fmt.Println("I have a little donkey, I never ride it")
			time.Sleep(2*time.Second)
		}
	}()
	select {
	}
}

四、线程池

github上有个封装的肥肠好的线程池:goroutine pool

4.1 创建goroutine池

定义
func NewPool(numWorkers int, jobQueueLen int) *Pool
第一个参数是工作goroutine数,第二个是整个池子的任务等待队列

4.2 未完持续

五、实际应用

最近的一个需求总结起来就是:

  1. 读取一个文件,文件每行可以看成是一个请求数据
  2. 程序需要读取每行的数据然后用get请求到服务器,并获取到服务器返回来的数据
  3. 然后将服务器的数据与原来的请求数据放到同一行输出到另一个文件,要求:顺序可以错乱,可以有重复,但必须全部都要写入。
  4. 但是因为数据量比较大,有时需要断开读取作别的事,因此再次回来重新读的时候需要回到之前读过的位置。
  5. 因此,读过的位置需要存储到log文件中,下次重启读取需要从这一行开始往下读。

解决思路:

  1. 文件读取用go语言官方库提供的bufio.Scanner,根据提供的文件开头,开始读取文件前,现将其滑动到对应的行
  2. 因为读取需要请求网络,不一定马上的得到相应,如果只用main主goroutine势必会因为网络问题阻塞,因此发送请求并处理的过程分派到多个goroutine去做,将每个结果输入到一个叫WriteChan的通道里面。对于多goroutine的管理,我采用的是用goroutine池去管理。
  3. 再开启一个写goroutine,读取WriteChan的值,然后写入磁盘文件
  4. 对于如何实现断开再重启,主要有两个问题:
    1)获取每一步读取的最小行数
    2) 完成一个任务需要将任务从队列中删除
    我的解决方式就是用一个map+一个双向队列,每个任务都有一个编号,可以作为map的key,任务是一个结构体,尤其前导和后继节点,链接成一个双相链表:
    1)对于加任务:将任务放到队列末尾,并放到map里面,key为编号,val为结构体的指针
    2)对于取任务:将任务从链表中删除,并将其从map中取出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值