golang多线程之精巧

遇到一个数据查找问题,不算很难,但是使用golang的context包和goroutine相关内容来实现令人无比舒适,下面看题目:

假设有一个超长的切片(其他语言的同学理解为数组即可),切片的元素类型为int,切片中的元素为乱序排列。限时5秒,使用多线程查找切片中是否存在给定值,在找到目标值或者超时后立刻结束所有的查找任务。

下面来演示解决方法,首先进行一点点背景介绍,go对多线程进行了协程封装(goroutine),goroutine间数据通讯建议使用通道(channel,一种可以有缓冲的队列),context包提供了多线程上下文控制的能力,select是一个可以对通道通讯进行switch的控制结构,通过这几块内容的组合,可以优雅精巧的实现题目的解决,下面直接看思路:

  1. 首先题目里提到了在找到目标值或者超时后立刻结束所有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()
    }
}
  1. 执行任务的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
        }
    }

}

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

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

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

resultChan <- true

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

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

  1. 下面是完整代码:
package main

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

func main() {
    timer := time.NewTimer(time.Second * 5)
    data := []int{1, 2, 3, 10, 999, 8, 345, 7, 98, 33, 66, 77, 88, 68, 96}
    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()
    }
}

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

}

实在是太精妙了。


发表评论

电子邮件地址不会被公开。 必填项已用*标注