MST

星途 面试题库

面试题:进程管理:多线程程序中共享资源引发的问题及解决方案

假设你正在开发一个多线程应用程序,其中多个线程会访问共享资源,可能会出现什么问题?请说明常见的解决这些问题的技术手段及其原理。
49.4万 热度难度
操作系统进程管理

知识考点

AI 面试

面试题答案

一键面试

可能出现的问题

  1. 竞态条件(Race Condition):多个线程同时访问和修改共享资源,由于执行顺序的不确定性,导致最终结果依赖于线程执行的顺序,产生不可预测的错误。
  2. 死锁(Deadlock):两个或多个线程相互等待对方释放资源,形成一种僵持状态,导致所有相关线程都无法继续执行。
  3. 饥饿(Starvation):某些线程由于优先级过低,长时间无法获得CPU资源,从而一直无法执行。

常见解决技术手段及其原理

  1. 互斥锁(Mutex)
    • 原理:通过一个二元信号量(0或1)来控制对共享资源的访问。当一个线程获取到互斥锁(信号量设为0),其他线程就无法获取,直到该线程释放互斥锁(信号量设为1)。这样保证同一时间只有一个线程可以访问共享资源,避免竞态条件。
    • 示例代码(以C++为例)
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_variable = 0;

void increment() {
    mtx.lock();
    shared_variable++;
    mtx.unlock();
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final value: " << shared_variable << std::endl;
    return 0;
}
  1. 读写锁(Read - Write Lock)
    • 原理:区分读操作和写操作。允许多个线程同时进行读操作,因为读操作不会修改共享资源,不会产生竞态条件。但是写操作时,为了保证数据一致性,需要独占资源,不允许其他读写操作。读写锁通过维护一个读计数和一个写标志来实现这种控制。
    • 示例代码(以Java为例)
import java.util.concurrent.locks.ReentrantReadWriteLock;

class SharedResource {
    private int data;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public int read() {
        lock.readLock().lock();
        try {
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void write(int newData) {
        lock.writeLock().lock();
        try {
            data = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}
  1. 条件变量(Condition Variable)
    • 原理:条件变量通常和互斥锁一起使用。线程可以在条件变量上等待某个条件满足,当另一个线程改变了相关条件后,通过条件变量通知等待的线程。等待线程被唤醒后,重新获取互斥锁,检查条件是否真的满足,然后进行相应操作。这有助于解决线程间的同步问题,避免无效的循环等待。
    • 示例代码(以Python为例)
import threading

condition = threading.Condition()
shared_state = 0

def consumer():
    global shared_state
    with condition:
        while shared_state == 0:
            condition.wait()
        print(f"Consumer got: {shared_state}")

def producer():
    global shared_state
    with condition:
        shared_state = 42
        condition.notify()

t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)

t1.start()
t2.start()

t1.join()
t2.join()
  1. 信号量(Semaphore)
    • 原理:信号量维护一个计数器,线程获取信号量时,计数器减1,当计数器为0时,其他线程无法获取信号量。释放信号量时,计数器加1。它可以控制同时访问共享资源的线程数量,不仅可以用于互斥(计数器设为1时等同于互斥锁),还可以用于限制并发访问数量。
    • 示例代码(以C#为例)
using System;
using System.Threading;

class Program {
    static Semaphore semaphore = new Semaphore(2, 2); // 初始计数为2,最大计数为2
    static void Worker() {
        semaphore.WaitOne();
        try {
            Console.WriteLine("Thread {0} entered the critical section.", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(1000);
        } finally {
            semaphore.Release();
            Console.WriteLine("Thread {0} left the critical section.", Thread.CurrentThread.ManagedThreadId);
        }
    }

    static void Main() {
        for (int i = 0; i < 4; i++) {
            new Thread(Worker).Start();
        }
        Console.ReadKey();
    }
}
  1. 避免死锁的策略
    • 破坏死锁的四个必要条件
      • 互斥条件:尽量避免独占资源,如果资源可以共享使用,就不采用独占方式。
      • 占有并等待条件:要求线程一次性获取所有需要的资源,而不是逐步获取,避免持有部分资源时等待其他资源。
      • 不可剥夺条件:允许操作系统或其他机制剥夺线程持有的资源,例如当检测到死锁时,强制某个线程释放资源。
      • 循环等待条件:对资源进行排序,线程按照固定顺序获取资源,避免形成循环等待的情况。
    • 死锁检测与恢复:通过记录线程和资源的使用关系,定期检测是否存在死锁。如果检测到死锁,可以选择终止一个或多个线程来打破死锁,然后尝试恢复系统状态。
  2. 避免饥饿的策略
    • 动态优先级调整:根据线程等待时间动态调整线程优先级,等待时间越长,优先级越高,确保低优先级线程不会长时间得不到执行机会。
    • 公平调度算法:采用公平的调度算法,如时间片轮转调度算法,每个线程轮流获得相同的CPU时间片,避免某些线程被长期忽略。