路漫漫其修远兮
吾将上下而求索

go学习:常用小片段

每秒执行一次

package main

import (
   "fmt"
   "runtime"
   "time"
)

func main() {
   c := time.Tick(1 * time.Second)
   for range c {
      fmt.Printf("#goroutines: %d\n", runtime.NumGoroutine())
   }
}

下面的调度思路值得学习

先来回顾一下上周文章里思考题的题目:

假设有一个超长的切片,切片的元素类型为int,切片中的元素为乱序排列。限时5秒,使用多个goroutine查找切片中是否存在给定值,在找到目标值或者超时后立刻结束所有goroutine的执行。


比如切片为:[23, 32, 78, 43, 76, 65, 345, 762, …… 915, 86],查找的目标值为345,如果切片中存在目标值程序输出:"Found it!"并且立即取消仍在执行查找任务的goroutine。如果在超时时间未找到目标值程序输出:"Timeout! Not Found",同时立即取消仍在执行查找任务的goroutine

首先题目里提到了在找到目标值或者超时后立刻结束所有goroutine的执行,完成这两个功能需要借助计时器、通道和context才行。我能想到的第一点就是要用context.WithCancel创建一个上下文对象传递给每个执行任务的goroutine,外部在满足条件后(找到目标值或者已超时)通过调用上下文的取消函数来通知所有goroutine停止工作。

func main() {
    timer := time.NewTimer(time.Second * 5)
    ctx, cancel := context.WithCancel(context.Background())
    resultChan := make(chan bool)
  ......
    select {
    case <-timer.C:
        fmt.Fprintln(os.Stderr, "Timeout! Not Found")
        cancel()
    case <- resultChan:
        fmt.Fprintf(os.Stdout, "Found it!\n")
        cancel()
    }
}

执行任务的goroutine们如果找到目标值后需要通知外部等待任务执行的主goroutine,这个工作是典型的应用通道的场景,上面代码也已经看到了,我们创建了一个接收查找结果的通道,接下来要做的就是把它和上下文对象一起传递给执行任务的goroutine

func SearchTarget(ctx context.Context, data []int, target int, resultChan chan bool) {
    for _, v := range data {
        select {
        case <- ctx.Done():
            fmt.Fprintf(os.Stdout, "Task cancelded! \n")
            return
        default:
        }
        // 模拟一个耗时查找,这里只是比对值,真实开发中可以是其他操作
        fmt.Fprintf(os.Stdout, "v: %d \n", v)
        time.Sleep(time.Millisecond * 1500)
        if target == v {
            resultChan <- true
            return
        }
    }

}

在执行查找任务的goroutine里接收上下文的取消信号,为了不阻塞查找任务,我们使用了select语句加default的组合:

select {
case <- ctx.Done():
    fmt.Fprintf(os.Stdout, "Task cancelded! \n")
    return
default:
}

goroutine里面如果找到了目标值,则会通过发送一个true值给resultChan,让外面等待的主goroutine收到一个已经找到目标值的信号。

resultChan <- true

这样通过上下文的Done通道和resultChan通道,goroutine们就能相互通信了。

Go 语言中最常见的、也是经常被人提及的设计模式 — 不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存

完整的源代码如下:

package main

import (
    "context"
    "fmt"
    "os"
    "time"
)

func main() {
    timer := time.NewTimer(time.Second * 5)
    data := []int{123109998345798336677886896}
    dataLen := len(data)
    size := 3
    target := 345
    ctx, cancel := context.WithCancel(context.Background())
    resultChan := make(chan bool)
    for i := 0; i < dataLen; i += size {
        end := i + size
        if end >= dataLen {
            end = dataLen - 1
        }
        go SearchTarget(ctx, data[i:end], target, resultChan)
    }
    select {
    case <-timer.C:
        fmt.Fprintln(os.Stderr, "Timeout! Not Found")
        cancel()
    case <- resultChan:
        fmt.Fprintf(os.Stdout, "Found it!\n")
        cancel()
    }

    time.Sleep(time.Second * 2)
}

func SearchTarget(ctx context.Context, data []int, target int, resultChan chan bool) {
    for _, v := range data {
        select {
        case <- ctx.Done():
            fmt.Fprintf(os.Stdout, "Task cancelded! \n")
            return
        default:
        }
        // 模拟一个耗时查找,这里只是比对值,真实开发中可以是其他操作
        fmt.Fprintf(os.Stdout, "v: %d \n", v)
        time.Sleep(time.Millisecond * 1500)
        if target == v {
            resultChan <- true
            return
        }
    }

}

为了打印演示结果所以加了几处time.Sleep,这个程序更多的是提供思路框架,所以细节的地方没有考虑。有几位读者把他们的答案发给了我,其中有一位的提供的答案在代码实现上考虑的更全面,这个我们放到文末再说。

上面程序的执行结果如下:

v: 1 
v: 88 
v: 33 
v: 10 
v: 345 
Found it!
v: 2 
v: 999 
Task cancelded! 
v: 68 
Task cancelded! 
Task cancelded! 

因为是并发程序所以每次打印的结果的顺序是不一样的,这个你们可以自己试验一下。而且也并不是先开启的goroutine就一定会先执行,主要还是看调度器先调度哪个。

未经允许不得转载:江哥架构师笔记 » go学习:常用小片段

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址