跳转至

操作系统-多线程_多进程_线程池

3、多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁需要注意什么?

  1. 区别

(1)多线程并发执行多任务,需要考虑**同步**的问题;单线程不需要考虑同步的问题。 (2)一个进程里有多个线程,可以**并发**执行多个任务;一个进程里只有一个线程,就只能执行一个任务。 (3)多线程并发执行多任务,需要**切换内核栈与硬件上下文,有切换的开销**;单线程不需要切换,没有切换的开销。 (4)多线程从属于一个进程,单线程也从属于一个进程;一个线程挂掉都会导致从属的进程挂掉

  1. 多线程编程需要考虑**同步**的问题。线程间的同步方式包括**互斥锁、信号量、条件变量、读写锁**。
  2. 多线程加锁,主要需要注意**死锁**的问题。破坏死锁的必要条件从而避免死锁

5、若某进程中单个线程崩溃,一定会导致整个线程崩溃吗?

在C++中是这样的。

在C/C++语言中,所有线程共享进程地址空间,线程之间是完全透明的。一个线程崩溃就说明该线程此时已经出现了不可逆转的错误,该错误极有可能已经破坏掉了或者即将破坏掉进程的地址空间,因此在操作系统眼里看来继续运行其它线程是有很大风险的。

在 C/C++ 中,线程崩溃后可能导致整个进程崩溃的主要原因是线程共享的资源和环境问题。以下是一些可能的情况:

  1. 共享内存访问:多个线程可能同时访问同一个共享内存区域。如果**一个线程发生错误并修改了这个共享内存区域,其他线程可能会读取到无效或不一致的数据,导致程序逻辑出错或崩溃**。
  2. 未处理的异常:C/C++ 中的线程崩溃可能是由于**未处理的异常**引起的。如果一个线程抛出了未被捕获的异常,该线程将终止执行,并且异常很可能**会向上传播到进程级别**,导致整个进程崩溃。
  3. 信号处理:在某些系统中,当一个线程接收到某些特定的信号(如段错误)时,系统可能会**默认终止整个进程,以防止异常状态的影响扩散。这样做是为了保证进程的稳定性和安全性**。
  4. 资源泄漏:一个线程的崩溃可能导致资源泄漏,例如未释放的内存或未关闭的文件句柄。如果这些资源没有被正确清理和回收,进程可能会因为资源耗尽而崩溃。

要避免线程崩溃导致整个进程崩溃,需要合理设计和管理线程间的同步和通信机制,以及正确处理线程内部的异常。同时,编写健壮的代码,避免内存泄漏和资源泄漏也是非常重要的。

26、哲学家就餐问题

img

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

下面是一种**错误的解法**,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。

#define N 5

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

为了防止死锁的发生,可以设置两个条件:

  • 必须同时拿起左右两根筷子;
  • 只有在两个邻居都没有进餐的情况下才允许进餐。
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0           // 思考状态
#define HUNGRY   1           // 饥饿状态
#define EATING   2           // 进餐状态
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think(i);             // 哲学家思考
        take_two(i);          // 尝试拿起两把叉子
        eat(i);               // 进餐
        put_two(i);           // 放下两把叉子
    }
}

void take_two(int i) {
    down(&mutex);            // 进入临界区,互斥地操作 state 数组
    state[i] = HUNGRY;       // 设置哲学家状态为饥饿
    check(i);                // 尝试获取两把叉子并通知邻居
    up(&mutex);              // 离开临界区
    down(&s[i]);             // 如果没有成功获取叉子,等待通知,否则继续执行
}

void put_two(i) {
    down(&mutex);            // 进入临界区,互斥地操作 state 数组
    state[i] = THINKING;     // 设置哲学家状态为思考
    check(LEFT);             // 尝试通知左邻居
    check(RIGHT);            // 尝试通知右邻居
    up(&mutex);              // 离开临界区
}

void eat(int i) {
    down(&mutex);            // 进入临界区,互斥地操作 state 数组
    state[i] = EATING;       // 设置哲学家状态为进餐
    up(&mutex);              // 离开临界区
}

// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {         
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;   // 哲学家可以开始进餐
        up(&s[i]);            // 通知哲学家可以进餐
    }
}

30、说说线程池的设计思路,线程池中线程的数量由什么确定?

参考回答

  1. 设计思路: 实现线程池有以下几个步骤: (1)设置一个生产者消费者队列,作为临界资源。 (2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行 (3)当任务队列为空时,所有线程阻塞。 (4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通 知阻塞中的一个线程来处理

  2. 线程池中线程数量: 线程数量和哪些因素有关:CPU,IO、并行、并发

如果是CPU密集型应用,则线程池大小设置为:CPU数目+1
如果是IO密集型应用,则线程池大小设置为:2*CPU数目+1
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

所以**线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程**。

答案解析

  1. 为什么要创建线程池: 创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创 建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能**导致系统资源不足**。同时线 程池也是为了提升系统效率
  2. 线程池的核心线程与普通线程: 任务队列可以存放100个任务,此时为空;线程池里有10个核心线程,若突然来了10个任务,那 么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务 先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创 建20个普通线程来处理多余的任务。 以上是线程池的工作流程。