伴鱼面试题

1 用time.After和context实现,两个方法本质上都是用了context或chan进行传递信息,如果go协程结束了,则用cancel关闭协程或往channel中传入值,同时用case time.After进行阻塞,若go协程超时了,则会走case time.After通道,

两种方法,第一种是go协程里嵌套了go协程,第二种是先启动n个go协程去运行,再启动n个go协程去监控,

package main
import (
    "context"
    "fmt"
    "sync"
    "time"
)
//实现从三个网站爬取数据并用map保存的功能,如果协程超过1秒,则直接返回,
// 1 用time.After结合context实现,
// https://shockerli.net/post/golang-select-time-implement-timeout/
var res = make(map[string]string)
var rw sync.RWMutex
func writeMap(ctx context.Context, key string, cancel context.CancelFunc) {
    rw.Lock()
    res[key] = (" " + key + " 网站爬取结果")
    rw.Unlock()
    // 写这个是为了节省时间,因为有可能go协程的运行时间小于WithTimeout中所给的时间,
    // 导致程序已经取完数据了,go协程仍然没有结束,
    cancel()
}
// 这里必须要开启两个go协程,一个用于运行爬虫程序,另一个用于监控时间,其中一个case是time.After即超时的channel,
// 另一个是go协程结束后,会close ctx.Done,或者在规定的WithTimeout时间内go协程没有结束的话,WithTimeout内部会close ctx.Done
func doSomething(key string) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second * 4)
    // 这个是为了防止忘记关闭协程,导致内存泄漏,
    defer cancel()
    go writeMap(ctx, key, cancel)
    select {
    case <- ctx.Done():
        fmt.Println(key + "网站爬取完毕!!!")
    // 注意这个time.After返回的是一个time类型的chan,所以这里可以这样写,
    case <-time.After(time.Second * 5):
        fmt.Println(key + "网站爬取超时!!!")
    }
}
func main() {
    var url = []string{
        "www.baidu.com",
        "www.123.com",
        "www.456.com",
    }
    for _, num := range url {
        go doSomething(num)
    }
    time.Sleep(time.Second * 8)
    fmt.Println(res)
}
View Code

用time.After和chan实现

// 只用time.After实现,这个方法的关键是当goroutine结束时,向chan通道中传入一个string,之后再用一个case去读取,
// 如果能读到,则说明没有超时,否则走超时的case,
var res sync.Map
var wg sync.WaitGroup
// 需要缓冲,不然阻塞
var sign = make(chan string, 3)
func writeMap(key string) {
    res.Store(key, key+" 网站爬虫结果")
}
func doSomething(key string) {
    writeMap(key)
    sign <- key
}
func doSomething1(key string) {
    for {
        writeMap(key)
    }
    sign <- key
}
func main() {
    var url = []string{
        "www.baidu.com",
        "www.123.com",
        "www.456.com",
    }
    for index, num := range url {
        if index == 2 {
            go doSomething1(num)
        } else {
            // 用死循环查看超时的情况,
            go doSomething(num)
        }
    }
    wg.Add(len(url))
    for _, num := range url {
        go func() {
            defer wg.Done()
            select {
            case r := <-sign:
                fmt.Println(r + "网站爬取完毕!!!")
                return
            // 注意这个time.After返回的是一个time类型的chan,所以这里可以这样写,
            case <-time.After(time.Second * 3):
                fmt.Println(num + "网站爬取超时!!!")
                return
            }
        }()
    }
    wg.Wait()
}
View Code

参考:https://shockerli.net/post/golang-select-time-implement-timeout/ 这是类似第二种方法,只不过只有一个goroutine,

2 火线安全笔试题

A 用两个协程交替输出数字和字母,12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

发现这个题的一个问题是,两个协程的循环次数要一致,否则会出现死锁得情况,

思路一:关键是对channel的使用,channel有锁的作用,可以使用两个channel,协程1向chan2中写入,协程2向chan1中写入,chan1写在协程1的开头,chan2写在协程2的开头,这两个chan就像两个开关一样,互相控制着对方,协程1向chan2中写入数据,就像打开了协程2的开关,因为是两个协程交替执行,所以向chan2中写入数据要在协程1的末尾执行,

var chan1 = make(chan bool, 1)
var chan2 = make(chan bool, 1)
var index = make(chan bool, 1)
func func1() {
    for i := 1; i < 26;  i += 2 {
        // 2, 取走chan1里的元素,继续运行,打印两个数字
        <-chan1
        fmt.Print(i)
        fmt.Print(i + 1)
        // 3, 给chan2 放入一个元素,等待取走元素
        chan2 <- true
    }
    time.Sleep(2000000)
    index <- true
}
func func2() {
    for i := 'A'; i <= 'Z'; i += 2 {
        // 4, chan2 取出元素,执行打印两个字符 ,
        <-chan2
        fmt.Print(string(i))
        fmt.Print(string(i+1))
        // 5, chan1 接收一个元素,进入阻塞状态,等待取走元素,进入第2步,2345步一直循环直到打印完
        chan1 <- true
    }
    // 6, 结束循环,index通道接收一个元素,进入阻塞状态,等待取走
}
func main()  {
    // 1, chan1 里放一个值,进入阻塞状态,等待取走元素
    chan1 <- true
    go func1()
    go func2()
    // 7, index通道取走元素,继续往下执行
    <-index
    // 结果: 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ
}
View Code

思路二:也可以用waitgroup,结合defer更加优雅,

// 输出数字
func func1(){
    defer wg.Done()
    for i := 1; i < 26; i += 2{
        fmt.Print(i)
        fmt.Print(i+1)
        chan2 <- true
        <- chan1
    }
}
// 输出字母
func func2(){
    defer wg.Done()
    for i := 'A'; i < 'Z'; i += 2{
        <- chan2
        fmt.Print(string(i))
        fmt.Print(string(i+1))
        chan1 <- true
    }
}
var wg sync.WaitGroup
var chan1 = make(chan bool, 1)
var chan2 = make(chan bool, 1)
func main(){
    wg.Add(2)
    go func1()
    go func2()
    wg.Wait()
}
View Code

 参考:https://blog.csdn.net/swan_tang/article/details/103514633 

3 蓝湖面试题

实现将三个生产者生产的数字放到一个数组中,并打印输出,题目给定了生产者返回的是一个channel,所以用select来监控,

package main

import "fmt"

// 三个协程是生产者,将它们生产的数据存起来,
func Producer1() <-chan int{
    a := make(chan int, 1)
    a <- 1
    return a
}
func Producer2() <-chan int{
    a := make(chan int, 1)
    a <- 2
    return a
}
func Producer3() <-chan int{
    a := make(chan int, 1)
    a <- 3
    return a
}

func main(){
    res := []int{}
    cou := 10
    go Producer1()
    go Producer2()
    go Producer3()
    for cou > 0{
        select{
        case a1 := <- Producer1():
            fmt.Println(a1)
            res = append(res, a1)
            go Producer1()
        case a2 := <- Producer2():
            fmt.Println(a2)
            res = append(res, a2)
            go Producer1()
        case a3 := <- Producer3():
            fmt.Println(a3)
            res = append(res, a3)
            go Producer1()
        }
        cou -= 1
    }
    fmt.Println(res)
}
View Code

自己构想的如果返回不是channel,即只用一个channel来实现,为了实现三个go协程的退出,这里使用了ctx来进行控制,因为三个go协程里都是死循环,无法通过WaitGroup来实现协程的退出

package main

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

func Producer1(ctx context.Context){
    for{
        select {
        case <- ctx.Done():
            return
        default:
            ch <- 1
            time.Sleep(time.Second * 1)
        }
    }
}
func Producer2(ctx context.Context){
    for{
        select{
        case <- ctx.Done():
            return
        default:
            ch <- 2
            time.Sleep(time.Second * 1)
        }
    }
}
func Producer3(ctx context.Context){
    for{
        select {
        case <- ctx.Done():
            return
        default:
            ch <- 3
            time.Sleep(time.Second * 1)
        }
    }
}
var ch chan int
func main()  {
    ctx, cancel := context.WithCancel(context.Background())
    res := []int{}
    ch = make(chan int, 5)
    timeout := time.After(time.Second * 2)
    go Producer1(ctx)
    go Producer2(ctx)
    go Producer3(ctx)
    for{
        select{
          case a := <- ch:
              res = append(res, a)
              continue
          case <- timeout:
              // 这种写法是错误的,会每次都重新计时
          //case <- time.After(time.Millisecond * 2):
              fmt.Println("abc")
        }
        // 注意这里break是专门为第二个case准备的,第一个case进去后会执行continue,
        // 这个break必须有,没有的话会死循环,也无法发挥定时器的作用,
        break
    }
    fmt.Println(res)
    cancel()
}
View Code

4 LeetCode  https://leetcode-cn.com/problems/print-zero-even-odd/comments/

输入5 打印0102030405

思路:func0控制func1和func2,func1 func2控制func0,只不过添加了个sign用于判断是该开启func1还是func2,

package main

import (
    "fmt"
    "sync"
)

// 打印0与奇偶数,
// 输入:n = 5
// 输出:"0102030405"
func func1(){
    for i := 0; i < n; i++ {
        <- ch0
        fmt.Print(0)
        if sign == 1{
            sign = 2
            ch1 <- true
        } else {
            sign = 1
            ch2 <- true
        }
        wg.Done()
    }
}
// 生成奇数
func func2(){
    for i := 1; i <= n; i += 2{
        <- ch1
        fmt.Print(i)
        ch0 <- true
        wg.Done()
    }
}
// 生成偶数
func func3(){
    for i := 2; i <= n; i += 2{
        <- ch2
        fmt.Print(i)
        ch0 <- true
        wg.Done()
    }
}
var ch0 = make(chan bool, 1)
var ch1 = make(chan bool, 1)
var ch2 = make(chan bool, 1)
var sign int
var wg sync.WaitGroup
var n int
func main()  {
    n = 9
    sign = 1
    wg.Add(2 * n)
    // 这个相当于一个启动子,类似于总开关,
    // 先让func0工作,func1和func2才能工作
    ch0 <- true
    go func1()
    go func2()
    go func3()
    wg.Wait()
}
View Code

 

posted on 2021-01-09 18:42  吃我一枪  阅读(279)  评论(0编辑  收藏  举报

导航