MST

星途 面试题库

面试题:Go条件变量与互斥锁的协同问题

在使用Go的条件变量时,经常会和互斥锁配合使用。请解释为什么需要这样配合,以及在并发场景下,如何正确地使用互斥锁和条件变量来避免死锁或数据竞争问题,并通过一个较为复杂的并发场景示例代码说明。
20.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

为什么需要互斥锁和条件变量配合使用

  1. 条件变量的作用:条件变量用于线程间的同步,它允许一个或多个线程等待特定条件的满足。例如,一个线程可能在等待某个资源可用,或者等待某个事件发生。
  2. 互斥锁的作用:互斥锁用于保护共享资源,防止多个线程同时访问共享资源而导致数据竞争。
  3. 配合原因:条件变量本身并不提供对共享资源的保护。当一个线程等待条件变量时,它需要释放对共享资源的锁(否则其他线程无法修改共享资源以满足条件),而当条件满足时,等待的线程需要重新获取锁才能安全地访问共享资源。因此,需要互斥锁与条件变量配合使用,确保共享资源在多线程环境下的安全访问。

如何正确使用避免死锁和数据竞争

  1. 初始化:正确初始化互斥锁和条件变量。
  2. 等待条件:在等待条件变量之前,先获取互斥锁。然后,在调用条件变量的等待方法时,该方法会自动释放互斥锁并将线程挂起。当条件满足被唤醒时,等待的线程会重新获取互斥锁。
  3. 通知条件:当某个线程改变共享资源状态使得条件满足时,先获取互斥锁,修改共享资源,然后通知条件变量。通知完成后释放互斥锁。

复杂并发场景示例代码

package main

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

type TaskQueue struct {
    tasks []int
    mutex sync.Mutex
    cond  sync.Cond
}

func (q *TaskQueue) AddTask(task int) {
    q.mutex.Lock()
    defer q.mutex.Unlock()
    q.tasks = append(q.tasks, task)
    q.cond.Broadcast()
}

func (q *TaskQueue) ProcessTasks(wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        q.mutex.Lock()
        for len(q.tasks) == 0 {
            q.cond.Wait()
        }
        task := q.tasks[0]
        q.tasks = q.tasks[1:]
        q.mutex.Unlock()

        fmt.Printf("Processing task: %d\n", task)
        time.Sleep(time.Second)
    }
}

func main() {
    var wg sync.WaitGroup
    taskQueue := &TaskQueue{}
    taskQueue.cond.L = &taskQueue.mutex

    numWorkers := 3
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go taskQueue.ProcessTasks(&wg)
    }

    for i := 1; i <= 10; i++ {
        taskQueue.AddTask(i)
    }

    wg.Wait()
}

在这个示例中:

  1. TaskQueue 结构体包含一个任务队列 tasks,一个互斥锁 mutex 和一个条件变量 cond
  2. AddTask 方法在向任务队列添加任务时,先获取互斥锁,添加任务后通过条件变量广播通知等待的线程。
  3. ProcessTasks 方法在处理任务时,先获取互斥锁,当任务队列为空时,通过条件变量等待。被唤醒后获取任务并处理。
  4. main 函数中,启动了多个工作线程来处理任务,并向任务队列中添加任务。通过这种方式,演示了在复杂并发场景下如何正确使用互斥锁和条件变量避免死锁和数据竞争。