golang
-
go和php的区别
类型:go为编译性语言;php解释性语言
错误:go的错误处理机制;php本身或者框架即可纠错
性能:go重视并发性能 php重视开发速度
应用:go侧重于容器/高性能并发/云计算/;php侧重于后台/网站/系统
-
编译型代码和解释型代码的区别
- 编译型和解释型语言的不同:过程发生的时机不一样。
- 编译型语言的代表是C,源代码被编译之后生成中间文件(.o和.obj),然后用连接器和汇编器生成机器码,也就是一系列基本操作的序列,机器码最后被执行生成最终动作。
- 解释型的语言以Ruby为例,也经历了这些步骤,不同的是,C语言会把那些从源代码“变”来的基本操作序列(保存)起来,而Ruby直接将这些生成的基本操作序列(Ruby虚拟机)指令丢给Ruby虚拟机执行然后产生动作了。
-
go的锁:
一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性。
一个互斥锁可以被用来保护一个临界区或者一组相关临界区。我们可以通过它来保证,在同一时刻只有一个 goroutine 处于该临界区之内。为了兑现这个保证,每当有 goroutine 想进入临界区时,都需要先对它进行锁定,并且,每个 goroutine 离开临界区时,都要及时地对它进行解锁。
sync包中的Mutex就是与其对应的类型,该类型的值可以被称为互斥量或者互斥锁。互斥锁是开箱即用的。换句话说,一旦我们声明了一个sync.Mutex类型的变量,就可以直接使用它了。
保证多个 goroutine 并发地访问同一个共享资源时的完全串行,这是通过保护针对此共享资源的一个临界区,或一组相关临界区实现的。因此,我们可以把它看做是 goroutine 进入相关临界区时,必须拿到的访问令牌。
- 使用互斥锁的注意事项如下:
不要重复锁定互斥锁;
不要忘记解锁互斥锁,必要时使用defer语句;
不要对尚未锁定或者已解锁的互斥锁解锁;
不要在多个函数之间直接传递互斥锁。
<img src="https://static001.geekbang.org/resource/image/73/6c/73d3313640e62bb95855d40c988c2e6c.png" alt="img" style="zoom:35%;" />
-
go的interface是怎么实现的?
只要目标类型方法集内包含接口声明的全部方法。
当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等等。总之,接口变量被赋予动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。
一个简单的逻辑就是需要获取这个类型的所有方法集合(集合A),并获取该接口包含的所有方法集合(集合B),然后判断列表B是否为列表A的子集。
-
golang的并发模式
goroutine是go的轻量级线程实现。
CSP (Communicating Sequential Process,通讯顺序进程) 模型不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。
-
通过channel通知实现并发控制
func main() { ch := make(chan struct{}) go func() { fmt.Println("do something..") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("I am finished") }
-
通过sync包中的WaitGroup实现并发控制
在
sync
包中,提供了WaitGroup
,它会等待它收集的所有goroutine
任务全部完成。在WaitGroup里主要有三个方法- Add, 可以添加或减少 goroutine的数量
- Done, 相当于Add(-1)
- Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0
func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() }
-
Context。
goroutine
的上下文。它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个Context
里,再将它传给要执行的goroutine
。在每一个循环中产生一个
goroutine
,每一个goroutine
中都传入context
,在每个goroutine
中通过传入ctx
创建一个子Context
,并且通过select
一直监控该Context
的运行情况,当在父Context
退出的时候,代码中并没有明显调用子Context
的Cancel
函数,但是分析结果,子Context
还是被正确合理的关闭了,这是因为,所有基于这个Context
或者衍生的子Context
都会收到通知,这时就可以进行清理操作了,最终释放goroutine
,这就优雅的解决了goroutine
启动后不可控的问题。package main import ( "context" "fmt" "sync" "time" ) type Message struct { netId int Data string } type ServerConn struct { sendCh chan Message handleCh chan Message wg *sync.WaitGroup ctx context.Context cancel context.CancelFunc netId int } func main() { conn := &ServerConn{ sendCh: make(chan Message), handleCh: make(chan Message), wg: &sync.WaitGroup{}, netId: 100, } conn.ctx, conn.cancel = context.WithCancel(context.WithValue(context.Background(), "key", conn.netId)) loopers := []func(*ServerConn, *sync.WaitGroup){readLoop, writeLoop, handleLoop} for _, looper := range loopers { conn.wg.Add(1) go looper(conn, conn.wg) } go func() { time.Sleep(time.Second * 3) conn.cancel() }() conn.wg.Wait() } func readLoop(c *ServerConn, wg *sync.WaitGroup) { netId, _ := c.ctx.Value("key").(int) handlerCh := c.handleCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { time.Sleep(time.Second * 1) select { case <-cDone: fmt.Println("readLoop close") return default: handlerCh <- Message{netId, "Hello world"} } } } func handleLoop(c *ServerConn, wg *sync.WaitGroup) { handlerCh := c.handleCh sendCh := c.sendCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { select { case handleData, ok := <-handlerCh: if ok { handleData.netId++ handleData.Data = "I am whole world" sendCh <- handleData } case <-cDone: fmt.Println("handleLoop close") return } } } func writeLoop(c *ServerConn, wg *sync.WaitGroup) { sendCh := c.sendCh ctx, _ := context.WithCancel(c.ctx) cDone := ctx.Done() defer wg.Done() for { select { case sendData, ok := <-sendCh: if ok { fmt.Println(sendData) } case <-cDone: fmt.Println("writeLoop close") return } } }
-
-
goroutine、channel。
Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个goroutine:主goroutine。当程序启动时,它会自动创建。goroutine是Go语言的基本调度单位,而channel则是它们之间的通信机制。操作符<-用来指定管道的方向,发送或接收。
Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。Go 语言 运行时的调度器是一个复杂的软件,能管理被创建的所有 goroutine 并为其分配执行时间。这个调度 器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行 goroutine。调度器在任何给定的时间,都会全面控制哪个 goroutine 要在哪个逻辑处理器上运行。
Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP) 的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是 对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。对于没有使用过通道写并发程序的程序员来说,通道会让他们感觉神奇而兴奋。希望读 者使用后也能有这种感觉。使用通道可以使编写并发程序更容易,也能够让并发程序出错更少。
-
线程和协程
线程:
当运行一个应用程序的时候,操作系统会给这个应用程序启动一个进程。我们可以将进程看作一个包含应用程序在运行中需要用到和维护的各种资源的容器。一个进程至少包含一个线程,这个线程就是主线程。操作系统会调度线程到不同的CPU上执行,这个CPU不一定就是进程所在的CPU。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200402232450525.png" alt="image-20200402232450525" style="zoom:40%;" />
- 进程:资源的所有权
- 线程:执行和调度的基本单位
- 同一进程下的各个线程共享资源,但寄存器、栈、PC不共享
协程:也有人称之为轻量级线程,具备以下几个特点:
- 能够在单一的系统线程中模拟多个任务的并发执行。
- 在一个特定的时间,只有一个任务在运行,即并非真正地并行。
- 被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外部没有办法中止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让 CPU使用权。
- 每个协程都有自己的堆栈和局部变量。
每个协程都包含3种运行状态:挂起、运行和停止。停止通常表示该协程已经执行完成。每个协程都包含3种运行状态:挂起、运行和停止。停止通常表示该协程已经执行完成(包括遇到问题明确执行退出),挂起则表示该协程尚未执行完成,但出让了时间片,以后 有机会时会由调度器继续执行。
线程与协程的区别:
- 可控的切换时机,一旦创建完线程,你就无法决定他什么时候获得时间片,什么时候让出时间片了,你把它交给了内核。而协程可以有。
- 很小的切换代价,从操作系统有没有调度权上看,协程就是因为不需要进行内核态的切换,所以会使用它,会有这么个东西。
-
Golang 的协程信通讯方式有哪些。
- 全局共享变量
- channel通信
- Context包
-
垃圾回收机制
- 三色标记法是对标记阶段的改进
- 初始状态所有对象都是白色。
- 从root根出发扫描所有根对象(下图a,b),将他们引用的对象标记为灰色(图中A,B)。root区域主要是程序运行到当前时刻的栈和全局数据区域。
- 分析灰色对象是否引用了其他对象。如果没有引用其它对象则将该灰色对象标记为黑色(上图中A);
- 如果有引用则将它变为黑色的同时将它引用的对象也变为灰色(上图中B引用了D)
重复步骤3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
- 三色标记法是对标记阶段的改进
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000810827.png" alt="image-20200403000810827" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000829012.png" alt="image-20200403000829012" style="zoom:50%;" />
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000857955.png" alt="image-20200403000857955" style="zoom:50%;" /><img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403000921980.png" alt="image-20200403000921980" style="zoom:50%;" />
-
GC流程
其实是因为Golang GC的大部分处理是和用户代码并行的。GC期间用户代码可能会改变某些对象的状态,如何实现GC和用户代码并行呢?先看下GC工作的完整流程:
Mark: 包含两部分:
Mark Prepare: 初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等。这个过程需要STW
GC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行
Mark Termination: 完成标记工作,重新扫描(re-scan)全局指针和栈。因为Mark和用户程序是并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan 再检查一下。这个过程也是会STW的。
Sweep: 按照标记结果回收所有的白色对象,该过程后台并行执行
Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC。
如果标记期间用户逻辑改变了刚打完标记的对象的引用状态,怎么办呢。
就是在每一轮GC开始时会初始化一个叫做“屏障”的东西,然后由它记录第一次scan时各个对象的状态,以便和第二次re-scan进行比对,引用状态变化的对象被标记为灰色以防止丢失,将屏障前后状态未变化对象继续处理。
-
GC 触发时机
- 超过内存大小阈值
- 达到定时时间 阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。
-
GPM并发调度
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003427220.png" alt="image-20200403003427220" style="zoom:40%;" />
首先是 Processor(简称 P),其作用类似于 CPU 核,用来控制可同时并发执行的任务数量。每个工作线程都必须绑定一个有效 P 才被允许执行任务,否则只能休眠,直到有空闲 P 时被唤醒。P 还为线程提供执行资源,比如对象分配内存、本地任务队列等。线程独享所绑定的P资源,可在无锁状态下执行高效操作。
基本上,进程内的一切都在以 goroutine(简称 G)方式运行,包括运行时相关服务,以及 main.main 入口函数。需要指出,G并非执行体,它仅仅保存并发任务状态,为任务执行提供所需栈内存空间。G 任务创建后被放置在P本地队列或全局队列,等待工作线程调度执行。
实际执行体是系统线程(简称 M),它和P绑定,以调度循环方式不停执行G 并发任务。M通过修改寄存器,将执行栈指向G自带的栈内存,并在此空间内分配堆栈帧,执行任务函数。当需要中途切换时,只要将相关寄存器值保存回G空间即可维护状态,任务M都可据此恢复执行。线程仅负责执行,不再持有状态,这是并发任务跨线程调度,实现多路复用的根本所在。
尽管 P/M 构成执行组合体,但两者数量并非一一对应。通常情况下,P 的数量相对恒定,默认与CPU核数量相同,但也可能更多或更少,而M则是由调度器按需创建的。举例来说,当M 因陷入系统调用而长时间阻塞时,P 就会被监控线程抢回,去新建(或唤醒)一个M执行其他任务,这样M的数量就会增长。
因为G初始栈仅有 2KB,且创建操作只是在用户空间简单地分配对象,远比进入内核态分配线程要简单得多。调度器让多个M进入调度循环,不停获取并执行任务,所以我们才能创建成千上万个并发任务。
操作系统线程、逻辑处理器和本地运行队列之间的关系。如果创建一个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403003703442.png" alt="image-20200403003703442" style="zoom:40%;" />
-
Golang 里的逃逸分析是什么?怎么避免内存逃逸?
- 定义
在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。
再往简单的说,Go是通过在编译器里做逃逸分析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上;即我发现变量在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配和回收比堆上快很多;反之,函数内的普通变量经过逃逸分析后,发现在函数退出后变量还有在其他地方上引用,那就将变量分配在堆上。做到按需分配。
- 存在目的
堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。堆适合不可预知大小的内存分配,这也意味着为此付出的代价是分配速度较慢,而且会形成内存碎片。
栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,因此栈的分配和回收速度非常快;我们常见的函数参数(不同平台允许存放的数量不同),局部变量等都会存放在栈上。
栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。
通俗比喻的说,栈就如我们去饭馆吃饭,只需要点菜(发出申请)--》吃吃吃(使用内存)--》吃饱就跑剩下的交给饭馆(操作系统自动回收),而堆就如在家里做饭,大到家,小到买什么菜,每一个环节都需要自己来实现,但是自由度会大很多。
我们就可以更好的知道逃逸分析存在的目的了:
1. 减少gc压力,栈上的变量,随着函数退出后系统直接回收,不需要gc标记后再清除。
2. 减少内存碎片的产生。
3. 减轻分配堆内存的开销,提高程序的运行速度。
- 避免内存逃逸
不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
-
slice底层原理
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有 3 个字段 的数据结构,这些数据结构包含 Go 语言需要操作底层数组的元数据。这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长 到的元素个数(即容量)。后面会进一步讲解长度和容量的区别。
<img src="/Users/anyao/Library/Application Support/typora-user-images/image-20200403005146656.png" alt="image-20200403005146656" style="zoom:40%;" />
-
Golang 的默认参数传递方式以及哪些是引用传递?
默认采用值传递,且Go 中函数传参仅有值传递一种方式。传参方式。slice、map、channel 都是引用类型。
for 和 for range 区别:for range 是值拷贝,随机遍历。
-
golang的特点
- 开发速度:使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。
- 并发:在 goroutine 之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。
- 类型系统:提供了灵活的、无继承的类型系统,无需降低运行性能就能最大程度上复用代码。还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。
- 内存管理:使用内存前要先分配这段内存,而且使用完毕后要将其释放掉。
Go 如何静态的去看线程是否安全:可以使用 go run -race 或者 go build -race来进行静态检测。
-
用户态和内核态
内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。
用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。
-
用户态切换为内核态的三种情况:
- 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口。
- 异常事件: 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
- 外围设备的中断:当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
channel缓冲的问题:带有缓冲(buffer)channel则可以非阻塞容纳N个元素。发送数据到缓冲(buffer) channel不会被阻塞,除非channel已满;同样的,从缓冲(buffer) channel取数据也不会被阻塞,除非channel空了。
-
golang的select有多个case时如何选择执行顺序。
- select机制简述
- select+case是用于阻塞监听goroutine的,如果没有case,就单单一个select{},则为监听当前程序中的goroutine,此时注意,需要有真实的goroutine在跑,否则select{}会报panic
- select底下有多个可执行的case,则随机执行一个。
- select常配合for循环来监听channel有没有故事发生。需要注意的是在这个场景下,break只是退出当前select而不会退出for,需要用break TIP / goto的方式。
- 无缓冲的通道,则传值后立马close,则会在close之前阻塞,有缓冲的通道则即使close了也会继续让接收后面的值
- 同个通道多个goroutine进行关闭,可用recover panic的方式来判断通道关闭问题
看完以上知识点其实还是没法解释本文的核心疑惑,继续往下!
- select机制简述
go select思想来源于网络IO模型中的select,本质上也是IO多路复用,只不过这里的IO是基于channel而不是基于网络,同时go select也有一些自己不同的特性:
1. 每个case都必须是一个通信
2. 所有channel表达式都会被求值
3. 所有被发送的表达式都会被求值
4. 如果任意某个通信可以进行,它就执行;其他被忽略。
5. 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。否则执行default子句(如果有)
6. 如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
-
golang的变量分配
-
Data segment:
- 初始化的全局变量或静态变量,会被分配在 Data 段。
- 未初始化的全局变量或静态变量,会被分配在 BSS 段。
- 在函数中定义的局部变量,会被分配在堆(Heap 段)或栈(Stack 段)。
- 实际上,如果考虑到 编译器优化,局部变量还可能会被 分配在寄存器,或者直接被 优化去掉。
-
对于 Go 而言,有两个地方可以用于分配:
堆(heap):由 GC 负责回收。对应于进程地址空间的堆。
栈(stack):不涉及 GC 操作。每个 goroutine 都有自己的栈,初始时被分配在进程地址空间的栈上,扩容时被分配在进程地址空间的堆上。
-
Go 变量主要分为两种:
全局变量:会被 Go 编译器标记为一些特殊的 符号类型,分配在堆上还是栈上目前尚不清楚,不过不是本文讨论的重点。
局部变量:对于在函数中定义的 Go 局部变量:要么被分配在堆上,要么被分配在栈上。
-
- Go 编译器会尽可能将变量分配在栈上,以下两种情况,Go 编译器会将变量分配在堆上:
- 如果一个变量被取地址(has its address taken),并且被逃逸分析(escape analysis)识别为 “逃逸到堆”(escapes to heap)
- 如果一个变量很大(very large)
- 结论:
- make([]struct{}, n) 只会被分配在栈上,而不会被分配在堆上。
- Brad Fitzpatrick 的注释是对的,并且他的意思是 “不会引发堆分配”。
- 逃逸分析识别出 escapes to heap,并不一定就是堆分配,也可能是栈分配。
- 进行内存分配器追踪时,如果采集不到堆分配信息,那一定只有栈分配。
- 如何确定一个 Go 变量会被分配在哪里?
- 先对代码作逃逸分析。
- 如果该变量被识别为 escapes to heap,那么它十有八九是被分配在堆上。
- 如果该变量被识别为 does not escape,或者没有与之相关的分析结果,那么它一定是被分配在栈上。
- 如果对 escapes to heap 心存疑惑,就对代码作内存分配器追踪。
- 如果有采集到与该变量相关的分配信息,那么它一定是被分配在堆上。否则,该变量一定是被分配在栈上。
-
怎么友好的关闭chan。
只由 sender 来关闭
一般不考虑关闭,除了一种情况:receiver 必须知道 sender 已经停止发送了
如果有多个发送者,就用一个 sync.WaitGroup,每次增加发送者时 Add,发送者结束时 Done,最后在需要关闭的时候 Wait 完再 close。通知发送者结束可以用 context.Context.Done。
-
生产者消费型,用channel通信。
package main import ( "fmt" ) var c = make(chan int, 50) var count = 0 func main() { for i := 0; i < 5; i++ { go consumer(i) } for i := 0; i < 1000; i++ { c <- i } /** here **/ fmt.Println(count) } func consumer(index int) { for target := range c { fmt.Printf("no.%d:%d\n", index, target) count++ } }
package main import ( "fmt" "sync" ) var c = make(chan int, 50) var count = 0 var wg = new(sync.WaitGroup) func main() { for i := 0; i < 5; i++ { wg.Add(1) go consumer(i) } for i := 0; i < 1000; i++ { c <- i } wg.Wait() close(c) /** here **/ fmt.Println(count) } func consumer(index int) { for target := range c { fmt.Printf("no.%d:%d\n", index, target) count++ if len(c) <= 0 { wg.Done() } } }
-
go实现协程池,用channel。
package main import "fmt" import "time" //***************************Task部分 //Task对外公开,所以要export type Task struct { f func() error //一个task代表一个任务,函数形式 } func NewTask(arg_f func() error) *Task { return &Task{ f:arg_f, } } func (t *Task) Execute() { t.f() } //***************************Pool部分 type Pool struct { EntryChan chan *Task //对外的任务入口 JobsChan chan *Task //内部的任务队列 workerNum int //协程池最大的worker数 } func NewPool(num int) *Pool { return &Pool{ EntryChan:make(chan *Task), JobsChan:make(chan *Task), workerNum:num, } } //Pool创建worker,由worker一直去JobsChan取任务并执行 func (p *Pool) worker(workerId int) { for task := range p.JobsChan { task.Execute() fmt.Println("workerID ", workerId, " 执行完了一个任务") } } //Pool运行 func (p *Pool) run() { //根据workerNum创建workers for i:=0;i<p.workerNum;i++ { go p.worker(i) //i作为workerId } //从EntryChan取任务,发送给JobsChan for task := range p.EntryChan { p.JobsChan <- task } } func main() { //创建任务 t := NewTask(func() error { fmt.Println(time.Now()) }) //创建协程池 p := NewPool(4) //把任务交给协程池 go func() { for { p.EntryChan <- t } } //启动协程池 p.run() }
有疑问加站长微信联系(非本文作者)