你会使用三个线程顺序打印数字吗?

前言

最近面试某个大厂,结果,问一个不会一个,并且编程题还写出了笑话,遭到了嘲笑。

是这样一个问题:

创建3个线程,按顺序依次打印1-100.即线程1打印1,线程2打印2,线程3打印3,然后线程1打印4,线程2打印5,线程3打印6......

其实问题不难,但是由于自己当时对 Go 不熟练,就写出了笑话,管道的操作符号(<-)写成了(->),对面的面试官笑的都憋不住了,唉,感觉受到了伤害。但是还要面对现实,看看这个问题怎么解决吧。

问题分析

其实这种问题本质是一个线程同步问题,通过线程同步工具,保证在并发环境下使得程序按序执行。

不同语言提供的线程同步工具有很多,最常见的就是锁,比如 Java 就有十多种锁可提供使用,如互斥锁,自旋锁,可重入锁,栅栏,信号量等。这里主要使用 Go 的互斥锁以及 channel 来提供 2 种解决方案。

实现方案

在讨论实现方案之前,一定要清楚一个线程相关的知识点:

对于普通的互斥锁,不强制之前对其加锁的线程来释放,可由其他获得 CPU 执行的线程来释放。

那么我们首先把问题拆解下,首先分析两个线程该如何打印。一个线程打印的是奇数,另一线程打印的是偶数,按照奇偶顺序打印。

线程要顺序执行,最常见的思路是通过锁来保证线程的同步顺序,只有线程获得锁才能完成执行动作,那么对于这个问需要设置 1 把锁还是 2 把锁呢?首先看设置 1 把锁的逻辑。

thread.drawio.png

假设,线程1先获得锁,可以设置一个标识,表明另一个线程是否执行完成,如果应该线程1应该执行,就打印数字,并设置标识下次应该由线程2执行;否则释放锁,由线程1和线程2继续抢占,直到线程2获得锁执行,之后重复这样的逻辑。代码如下。

package main

import (
    "fmt"
    "sync"
    "time"
)

type FooBar struct {
    mu              sync.Mutex
    wg              sync.WaitGroup
    firstJobDone    bool
}

func (fb *FooBar) printFirst() {
    for cnt := 1; cnt <= 100; {
        fb.mu.Lock()
        if fb.firstJobDone {
            fb.mu.Unlock()
        } else if cnt % 2 == 1 {
            fmt.Printf("Goroutine-1: %d\n", cnt)
            cnt += 2
            fb.firstJobDone = true
            fb.mu.Unlock()
        }
    }
}

func (fb *FooBar) printSecond() {
    for cnt := 2; cnt <= 100; {
        fb.mu.Lock()
        if !fb.firstJobDone {
            fb.mu.Unlock()
        } else if cnt % 2 == 0 {
            fmt.Printf("Goroutine-2: %d\n", cnt)
            cnt += 2
            fb.firstJobDone = false
            fb.mu.Unlock()
        }
    }
}

func main() {
    obj := FooBar{}
    go obj.printFirst()
    go obj.printSecond()
    obj.wg.Wait()
    // 一定要设置time.Sleep(),防止主线程退出后导致子协程没有执行完,程序就结束了
    time.Sleep(time.Second * 10)
}
复制代码

但是这样实现有明显的问题,就是抢占到线程后,发现不应该执行又释放线程,效率就会变得很差。所以,再看下设置2把锁的情况。

thread.drawio (1).png

为了保证线程1先执行,首先锁定线程2。线程1获得锁后,打印,释放线程2的锁;线程2获得锁,打印,释放线程1的锁。这样就不会出现无效获得锁释放锁的问题。

那么把问题拓展到 3 个线程顺序打印,也就是使用 3 把锁就可以了,通过一个线程的执行去设置另外两个线程的执行权力。接下来看代码。

package main

import (
    "fmt"
    "sync"
    "time"
)

type FooBar struct {
    mutexFirst      sync.Mutex
    mutexSecond     sync.Mutex
    mutexThird      sync.Mutex
    wg              sync.WaitGroup
}

func (fb *FooBar) construct() {
    // 先让线程2和线程3不能执行
    fb.mutexSecond.Lock()
    fb.mutexThird.Lock()
}

func (fb *FooBar) PrintFirst(pool int) {
    for i := 1; i <= pool; i += 3 {
        fb.mutexFirst.Lock()
        fmt.Printf("Goroutine-1: %d\n", i)
        fb.mutexSecond.Unlock()
    }
}

func (fb *FooBar) PrintSecond(pool int) {
    for i := 2; i <= pool; i += 3 {
        fb.mutexSecond.Lock()
        fmt.Printf("Goroutine-2: %d\n", i)
        fb.mutexThird.Unlock()
    }
}

func (fb *FooBar) PrintThird(pool int) {
    for i := 3; i <= pool; i += 3 {
        fb.mutexThird.Lock()
        fmt.Printf("Goroutine-3: %d\n", i)
        fb.mutexFirst.Unlock()
    }
}

func main() {
    obj := FooBar{}
    obj.construct()

    pool := 100
    go obj.PrintFirst(pool)
    go obj.PrintSecond(pool)
    go obj.PrintThird(pool)
    obj.wg.Wait()
    
    time.Sleep(time.Second * 10)
}
复制代码

运行效果:

Goroutine-1: 1
Goroutine-2: 2
Goroutine-3: 3
Goroutine-1: 4
Goroutine-2: 5
Goroutine-3: 6
Goroutine-1: ...
Goroutine-2: ...
Goroutine-3: ...
Goroutine-2: 98
Goroutine-3: 99
Goroutine-1: 100
复制代码

管道

Go 提供了另一种并发模型(CSP),可以利用 channel 来实现协程间的通信。接下里使用无缓冲 chanenl 来实现同样的功能。

package main

import (
    "fmt"
    "time"
)

var POOL = 100

// 为什么每一个goroutine都要从从1运行到100呢?
// 因为每个数字都要传递到3个协程当中,如果有跳跃式的遍历,那么会导致后面的协程缺少数字
func printFirst(p chan int, r chan int) {
    for i := 1; i <= POOL; i++ {
        p <- i
        if i%3 == 1 {
            fmt.Println("groutine-1:", i)
        }
        <-r
    }
}

func printSecond(p chan int, q chan int) {
    for i := 1; i <= POOL+1; i++ {
        <-p
        if i%3 == 2 {
            fmt.Println("groutine-2:", i)
        }
        q <- i
    }
}

func printThird(q chan int, r chan int) {
    for i := 1; i <= POOL; i++ {
        <-q
        if i%3 == 0 {
            fmt.Println("groutine-3:", i)
        }
        r <- i
    }
}

func main() {
    msg := make(chan int)
    msg2 := make(chan int)
    msg3 := make(chan int)
    
    go printFirst(msg, msg3)
    go printSecond(msg, msg2)
    go printThird(msg2, msg3)

    time.Sleep(time.Second * 2)
}
复制代码

总结

并发编程是一个很大的话题,每种语言有自己的解决方案,为了应付面试,建议还是多刷刷 leetcode。

猜你喜欢

转载自juejin.im/post/7019484428623675429