leetcode刷题记录33(多线程专题)(2024-2-21)【按序打印(信号量、条件变量) | 交替打印 FooBar | 打印零与奇偶数 | 交替打印字符串(互斥量+条件变量) | 哲学家进餐】

1114. 按序打印

给你一个类:

public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}

三个不同的线程 A、B、C 将会共用一个 Foo 实例。

线程 A 将会调用 first() 方法
线程 B 将会调用 second() 方法
线程 C 将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

提示:

尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。

示例 1:

输入:nums = [1,2,3]
输出:“firstsecondthird”
解释:
有三个线程会被异步启动。输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。正确的输出是 “firstsecondthird”。

示例 2:

输入:nums = [1,3,2]
输出:“firstsecondthird”
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。正确的输出是 “firstsecondthird”。

提示:
nums 是 [1, 2, 3] 的一组排列

方法1:int信号量+while轮询

自己手工实现了一个信号量(int变量),加上while循环作为自旋操作,实现了3个线程之间的控制。

代码如下:

class Foo
{
    int firstDone = 0;
    int secondDone = 0;
    int i = 0;

public:
    Foo()
    {
    }

    void first(function<void()> printFirst)
    {

        // printFirst() outputs "first". Do not change or remove this line.
        printFirst();
        firstDone = 1;
    }

    void second(function<void()> printSecond)
    {
        while (firstDone != 1)
        {
            /* code */
        }

        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        secondDone = 1;
    }

    void third(function<void()> printThird)
    {
        while (secondDone != 1)
        {
            /* code */
        }

        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};

这个代码没有通过力扣的测试,但是,我自己对这个代码进行了测试,结果是经过多次测试,结果均为正确。猜测原因可能是由力扣的评判机制导致的。

测试程序的主函数定义如下:

int main()
{
    int numTest = 10;
    clock_t allTime = 0;
    for (int i = 0; i < numTest; i++)
    {
        // 计时开始
        clock_t startTime, endTime;
        startTime = clock();

        Foo foo;

        // Lambda 函数用于打印字符串
        auto printFirst = []
        { std::cout << "first" << std::endl; }; // Use std::cout instead of cout
        auto printSecond = []
        { std::cout << "second" << std::endl; }; // Use std::cout instead of cout
        auto printThird = []
        { std::cout << "third" << std::endl; }; // Use std::cout instead of cout

        // 创建三个线程,分别执行 first、second、third 函数
        thread t1(&Foo::third, &foo, printThird);
        thread t2(&Foo::first, &foo, printFirst);
        thread t3(&Foo::second, &foo, printSecond);

        // 等待三个线程执行完成
        t1.join();
        t2.join();
        t3.join();
        // 计时结束
        endTime = clock();
        allTime += endTime - startTime;
    }

    cout << "Average time: " << allTime / numTest << "ms" << endl;

    return 0;
}

虽然上边的代码经过测试,是可以对3个线程进行控制的。然而,该实现中使用了简单的轮询检查标志位的方式,这可能会导致性能问题,并且不是一个良好的多线程实践。

方法2:sem_t信号量+P/V操作

class Foo
{

protected:
    sem_t firstJobDone;
    sem_t secondJobDone;

public:
    Foo()
    {
        sem_init(&firstJobDone, 0, 0);
        sem_init(&secondJobDone, 0, 0);
    }

    void first(function<void()> printFirst)
    {
        // printFirst() outputs "first".
        printFirst();
        sem_post(&firstJobDone);
    }

    void second(function<void()> printSecond)
    {
        sem_wait(&firstJobDone);
        // printSecond() outputs "second".
        printSecond();
        sem_post(&secondJobDone);
    }

    void third(function<void()> printThird)
    {
        sem_wait(&secondJobDone);
        // printThird() outputs "third".
        printThird();
    }
};

这段代码是使用信号量(sem_t)实现的多线程同步控制,确保 first 函数在 second 函数之前执行,second 函数在 third 函数之前执行。

具体解释如下:

  1. sem_t firstJobDone;sem_t secondJobDone; 是两个信号量,分别用于标识第一个和第二个任务是否完成。

  2. 在构造函数 Foo() 中,通过 sem_init 初始化了两个信号量,初始值都为 0,表示两个任务都还未完成。

  3. void first(function<void()> printFirst):该函数执行 printFirst 并通过 sem_post(&firstJobDone) 使得 firstJobDone 信号量的值加一,表示第一个任务已经完成。

  4. void second(function<void()> printSecond):该函数首先通过 sem_wait(&firstJobDone) 等待 firstJobDone 信号量的值变为大于 0,即第一个任务已经完成。然后执行 printSecond 并通过 sem_post(&secondJobDone) 使得 secondJobDone 信号量的值加一,表示第二个任务已经完成。

  5. void third(function<void()> printThird):该函数首先通过 sem_wait(&secondJobDone) 等待 secondJobDone 信号量的值变为大于 0,即第二个任务已经完成。然后执行 printThird

这样,通过信号量的控制,确保了 firstsecond 之前执行,secondthird 之前执行。这是一种有效的同步机制,避免了轮询检查标志位的性能问题。

sem_wait 是 POSIX 线程库中用于等待信号量的函数,用于对信号量进行 P 操作(原语操作)。它的原型如下:

int sem_wait(sem_t *sem);
  • sem: 指向要等待的信号量的指针。

sem_wait 函数的作用是阻塞当前线程,直到对应的信号量的值变为非负数。一般用于等待其他线程或进程对信号量进行 V 操作(增加信号量的值)。具体的行为可以概括为:

  1. 如果信号量的值大于 0,sem_wait 将信号量的值减一,并立即返回。
  2. 如果信号量的值为 0,sem_wait 将阻塞当前线程,直到有其他线程或进程对信号量进行 V 操作,使得信号量的值变为非负数。

sem_wait 在等待期间可能被信号中断(被信号处理器中断),此时返回-1,并设置 errnoEINTR。这是为了处理异步信号的情况。

使用 sem_wait 通常需要与 sem_post 配合使用,sem_post 用于对信号量进行 V 操作,增加信号量的值,从而唤醒等待的线程。

在上面提到的 Foo 类的实现中,sem_wait 用于等待任务的完成,例如在 second 函数中,通过等待 firstJobDone 信号量,确保第一个任务已经完成。

方法3:信号量+锁+条件变量

class Foo
{
    int firstDone = 0;
    int secondDone = 0;
    mutex mtx;
    condition_variable cv;

public:
    Foo()
    {
    }

    void first(function<void()> printFirst)
    {
        {
            unique_lock<mutex> lock(mtx);
            // printFirst() outputs "first". Do not change or remove this line.
            printFirst();
            firstDone = 1;
        }
        cv.notify_all(); // 通知等待的线程
    }

    void second(function<void()> printSecond)
    {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [&]
                { return firstDone == 1; }); // 等待直到 firstDone 变为 1
        // printSecond() outputs "second". Do not change or remove this line.
        printSecond();
        secondDone = 1;
        cv.notify_all(); // 通知等待的线程
    }

    void third(function<void()> printThird)
    {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [&]
                { return secondDone == 1; }); // 等待直到 secondDone 变为 1
        // printThird() outputs "third". Do not change or remove this line.
        printThird();
    }
};

这个版本使用了 std::condition_variable 来进行线程间的通信,通过 wait 和 notify_all 来等待和通知线程。这样可以避免轮询,提高了效率。

std::unique_lock 是 C++ 标准库提供的一个模板类,用于在多线程环境中对互斥量进行管理。它提供了比 std::lock_guard 更灵活的功能,允许在构造和析构期间对互斥量进行加锁和解锁的操作。std::unique_lock 具有以下特点:

  1. 构造时加锁,析构时解锁:std::unique_lock 对象创建时(即构造函数调用时),它会自动对互斥量进行加锁;当 std::unique_lock 对象销毁时(即析构函数调用时),它会自动对互斥量进行解锁。

  2. 支持手动加锁和解锁: 除了构造和析构时的自动加锁和解锁外,std::unique_lock 还提供了成员函数 lock()unlock(),允许在其生命周期内手动控制锁的状态。

  3. 支持条件变量: std::unique_lock 对象可以与条件变量一起使用,通过 std::condition_variable 中的 wait() 函数等待条件,并在条件满足时自动解锁互斥量。

  4. 可传递给函数: std::unique_lock 对象可以作为参数传递给函数,使得锁的所有权可以在函数调用中传递。

使用 std::unique_lock 的一个常见场景是在多线程环境中对共享资源进行保护,确保线程安全。在上面的代码示例中,unique_lock<mutex> 用于创建一个 std::unique_lock 对象,并对 std::mutex 进行加锁和解锁操作。

1115. 交替打印 FooBar

给你一个类:

class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print(“foo”);
}
}

public void bar() {
for (int i = 0; i < n; i++) {
print(“bar”);
}
}
}

两个不同的线程将会共用一个 FooBar 实例:

线程 A 将会调用 foo() 方法,而
线程 B 将会调用 bar() 方法
请设计修改程序,以确保 “foobar” 被输出 n 次。

示例 1:

输入:n = 1
输出:“foobar”
解释:这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,“foobar” 将被输出一次。

示例 2:

输入:n = 2
输出:“foobarfoobar”
解释:“foobar” 将被输出两次。

提示:

1 <= n <= 1000

自己比较习惯用条件变量来实现:

#include <functional>
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>

using namespace std;

class FooBar
{
private:
    int n;
    int fooDone;
    mutex mtx;
    condition_variable cv;

public:
    FooBar(int n)
    {
        this->n = n;
        fooDone = 0;
    }

    void foo(function<void()> printFoo)
    {

        for (int i = 0; i < n; i++)
        {
            unique_lock<mutex> lk(mtx);
            cv.wait(lk, [&]
                    { return fooDone == 0; });
            // printFoo() outputs "foo". Do not change or remove this line.
            printFoo();
            fooDone = 1;
            cv.notify_all();
        }
    }

    void bar(function<void()> printBar)
    {

        for (int i = 0; i < n; i++)
        {
            unique_lock<mutex> lk(mtx);
            cv.wait(lk, [&]
                    { return fooDone == 1; });
            // printBar() outputs "bar". Do not change or remove this line.
            printBar();
            fooDone = 0;
            cv.notify_all();
        }
    }
};

int main()
{
    auto printFoo = []
    { cout << "Foo"; };
    auto printBar = []
    { cout << "Bar"; };

    FooBar fb(5);

    thread t1(&FooBar::foo, &fb, printFoo);
    thread t2(&FooBar::bar, &fb, printBar);

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

    return 0;
}

这道题目题解写的非常好,总结了5种不同的实现方法(互斥锁、信号量、条件变量、异步操作、原子操作),不再赘述:https://leetcode.cn/problems/print-foobar-alternately/solutions/1020308/c-duo-fang-fa-by-zhouzihong-zdvj/

其中,异步操作的代码如下:

class FooBar {
private:
    int n;
    promise<void>readyFoo,readyBar;
    future<void>futureFoo,futureBar;
public:
    FooBar(int n) {
        this->n = n;
        futureFoo=readyFoo.get_future();
        futureBar=readyBar.get_future();
    }
    void foo(function<void()> printFoo) {   
        for (int i = 0; i < n; i++) {
            printFoo();
            readyFoo.set_value();
            futureBar.get();
            promise<void>newReadyBar;
            future<void>newFutureBar;
            readyBar=move(newReadyBar);
            newFutureBar=readyBar.get_future();
            futureBar=move(newFutureBar);
        }
    }
    void bar(function<void()> printBar) {   
        for (int i = 0; i < n; i++) {
            promise<void>newReadyFoo;
            future<void>newFutureFoo;
            readyFoo=move(newReadyFoo);
            newFutureFoo=readyFoo.get_future();
            futureFoo=move(newFutureFoo);
            printBar();
            readyBar.set_value();
        }
    }
};

其中,promise和future的用法第一次见到,总结一下:

在C++中,std::futurestd::promise 是用于在多线程编程中进行异步任务的通信和同步的工具。它们通常与 std::threadstd::async 和其他多线程相关的类一起使用。

  1. std::future:

    • std::future 是一个模板类,用于获取异步操作的结果。
    • 它提供一种异步获取结果的机制,允许一个线程在另一个线程执行的任务完成后获取结果。
    • std::future 对象可以通过 std::promisestd::packaged_taskget_future() 函数获得。
    std::future<int> future_result = some_async_function();
    int result = future_result.get(); // 阻塞直到异步任务完成并获取结果
    
  2. std::promise:

    • std::promise 是一个模板类,用于设置异步任务的结果。
    • 它允许一个线程设置一个值或异常,并通过关联的 std::future 对象通知等待的线程。
    • std::promise 对象通常与 std::thread 一起使用,将结果传递给等待的线程。
    std::promise<int> result_promise;
    std::future<int> future_result = result_promise.get_future();//将promise和future关联到一起
    
    std::thread([](std::promise<int>& promise) {
        // 异步操作
        int result = compute_result();
        promise.set_value(result); // 设置异步操作结果
    }, std::ref(result_promise)).detach();
    
    int result = future_result.get(); // 阻塞直到异步任务完成并获取结果
    

通过 std::futurestd::promise,可以实现异步任务之间的通信,允许一个线程产生结果,而另一个线程等待并获取这些结果。这种机制使得多线程编程更加方便和灵活。

1116. 打印零与奇偶数

现有函数 printNumber 可以用一个整数参数调用,并输出该整数到控制台。

例如,调用 printNumber(7) 将会输出 7 到控制台。
给你类 ZeroEvenOdd 的一个实例,该类中有三个函数:zero、even 和 odd 。ZeroEvenOdd 的相同实例将会传递给三个不同线程:

线程 A:调用 zero() ,只输出 0
线程 B:调用 even() ,只输出偶数
线程 C:调用 odd() ,只输出奇数
修改给出的类,以输出序列 “010203040506…” ,其中序列的长度必须为 2n 。

实现 ZeroEvenOdd 类:

ZeroEvenOdd(int n) 用数字 n 初始化对象,表示需要输出的数。
void zero(printNumber) 调用 printNumber 以输出一个 0 。
void even(printNumber) 调用printNumber 以输出偶数。
void odd(printNumber) 调用 printNumber 以输出奇数。

示例 1:

输入:n = 2
输出:“0102”
解释:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 “0102”。

示例 2:

输入:n = 5
输出:“0102030405”

提示:

1 <= n <= 1000

class ZeroEvenOdd
{
private:
    int n;
    mutex mtx;
    int state;
    condition_variable cv;

public:
    ZeroEvenOdd(int n)
    {
        this->n = n;
        state = 0;
    }

    // printNumber(x) outputs "x", where x is an integer.
    void zero(function<void(int)> printNumber)
    {
        for (int i = 0; i < n; i++)
        {
            unique_lock<mutex> lk(mtx);
            cv.wait(lk, [&]
                    { return state == 0 || state == 2; });
            printNumber(0);
            state++;
            cv.notify_all();
        }
    }

    void even(function<void(int)> printNumber)
    {
        for (int i = 2; i <= n; i += 2)
        {
            unique_lock<mutex> lk(mtx);
            cv.wait(lk, [&]
                    { return state == 3; });
            printNumber(i);
            state = 0;
            cv.notify_all();
        }
    }

    void odd(function<void(int)> printNumber)
    {
        for (int i = 1; i <= n; i += 2)
        {
            unique_lock<mutex> lk(mtx);
            cv.wait(lk, [&]
                    { return state == 1; });
            printNumber(i);
            state++;
            cv.notify_all();
        }
    }
};

题解总结的各种方法(原子操作、互斥锁+条件变量、信号量)很全面,不再赘述:https://leetcode.cn/problems/print-zero-even-odd/solutions/922116/c-san-chong-fang-shi-by-desaweis-imvm/

1117. H2O 生成

现在有两种线程,氧 oxygen 和氢 hydrogen,你的目标是组织这两种线程来产生水分子。

存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。

氢和氧线程会被分别给予 releaseHydrogen 和 releaseOxygen 方法来允许它们突破屏障。

这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。

换句话说:

如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
书写满足这些限制条件的氢、氧线程同步代码。

示例 1:

输入: water = “HOH”
输出: “HHO”
解释: “HOH” 和 “OHH” 依然都是有效解。

示例 2:

输入: water = “OOHHHH”
输出: “HHOHHO”
解释: “HOHHHO”, “OHHHHO”, “HHOHOH”, “HOHHOH”, “OHHHOH”, “HHOOHH”, “HOHOHH” 和 “OHHOHH” 依然都是有效解。

提示:

3 * n == water.length
1 <= n <= 20
water[i] == ‘O’ or ‘H’
输入字符串 water 中的 ‘H’ 总数将会是 2 * n 。
输入字符串 water 中的 ‘O’ 总数将会是 n 。

自己比较习惯用互斥锁和条件变量来实现,代码如下:

#include <functional>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>

using namespace std;

class H2O
{
    int numWarter;
    int numHydrogen;
    mutex mtx;
    condition_variable cv;

public:
    H2O()
    {
        numWarter = 0;
        numHydrogen = 0;
    }

    void hydrogen(function<void()> releaseHydrogen)
    {
        unique_lock<mutex> lk(mtx);
        cv.wait(lk, [&]
                { return numHydrogen < 2; });
        // releaseHydrogen() outputs "H". Do not change or remove this line.
        releaseHydrogen();
        numHydrogen++;
        if (numWarter == 1 && numHydrogen == 2)
        {
            numWarter = 0;
            numHydrogen = 0;
            cv.notify_all();
        }
    }

    void oxygen(function<void()> releaseOxygen)
    {
        unique_lock<mutex> lk(mtx);
        cv.wait(lk, [&]
                { return numWarter < 1; });
        // releaseOxygen() outputs "O". Do not change or remove this line.
        releaseOxygen();
        numWarter++;
        if (numWarter == 1 && numHydrogen == 2)
        {
            numWarter = 0;
            numHydrogen = 0;
            cv.notify_all();
        }
    }
};

int main() {
    H2O h2o;

    auto releaseHydrogen = []() {
        std::cout << "H";
    };

    auto releaseOxygen = []() {
        std::cout << "O";
    };

    std::vector<std::thread> threads;

    // Create threads for hydrogen and oxygen molecules
    for (int i = 0; i < 6; ++i) {
        if (i % 3 == 0 || i % 3 == 1) {
            threads.emplace_back([&h2o, releaseHydrogen] {
                h2o.hydrogen(releaseHydrogen);
            });
        } else {
            threads.emplace_back([&h2o, releaseOxygen] {
                h2o.oxygen(releaseOxygen);
            });
        }
    }

    // Join all threads
    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

1195. 交替打印字符串

编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:

如果这个数字可以被 3 整除,输出 “fizz”。
如果这个数字可以被 5 整除,输出 “buzz”。
如果这个数字可以同时被 3 和 5 整除,输出 “fizzbuzz”。
例如,当 n = 15,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz。

假设有这么一个类:

class FizzBuzz {
public FizzBuzz(int n) { … } // constructor
public void fizz(printFizz) { … } // only output “fizz”
public void buzz(printBuzz) { … } // only output “buzz”
public void fizzbuzz(printFizzBuzz) { … } // only output “fizzbuzz”
public void number(printNumber) { … } // only output the numbers
}
请你实现一个有四个线程的多线程版 FizzBuzz, 同一个 FizzBuzz 实例会被如下四个线程使用:

线程A将调用 fizz() 来判断是否能被 3 整除,如果可以,则输出 fizz。
线程B将调用 buzz() 来判断是否能被 5 整除,如果可以,则输出 buzz。
线程C将调用 fizzbuzz() 来判断是否同时能被 3 和 5 整除,如果可以,则输出 fizzbuzz。
线程D将调用 number() 来实现输出既不能被 3 整除也不能被 5 整除的数字。

提示:

本题已经提供了打印字符串的相关方法,如 printFizz() 等,具体方法名请参考答题模板中的注释部分。

其实许多涉及到线程控制的多线程场景,具有隐串行性,就是虽然程序有这么多线程,但同一时刻,只能有一个线程在临界区工作,其它的需要等待。因此,我们设置一个numDone,表示已经打印完成的数字序号,从0开始,表示一个也没有打印完成。

代码如下:

class FizzBuzz
{
private:
    int n;
    int numDone = 0;
    mutex mtx;
    condition_variable cv;

public:
    FizzBuzz(int n)
    {
        this->n = n;
        numDone = 0;
    }

    // printFizz() outputs "fizz".
    void fizz(function<void()> printFizz)
    {
        for (int i = 1; i <= n; i++)
        {
            if (i % 3 == 0 && i % 5 != 0)
            {
                unique_lock<mutex> lk(mtx);
                cv.wait(lk, [&]
                        { return numDone == i - 1; });
                printFizz();
                numDone++;
                cv.notify_all();
            }
        }
    }

    // printBuzz() outputs "buzz".
    void buzz(function<void()> printBuzz)
    {
        for (int i = 1; i <= n; i++)
        {
            if (i % 3 != 0 && i % 5 == 0)
            {
                unique_lock<mutex> lk(mtx);
                cv.wait(lk, [&]
                        { return numDone == i - 1; });
                printBuzz();
                numDone++;
                cv.notify_all();
            }
        }
    }

    // printFizzBuzz() outputs "fizzbuzz".
    void fizzbuzz(function<void()> printFizzBuzz)
    {
        for (int i = 1; i <= n; i++)
        {
            if (i % 3 == 0 && i % 5 == 0)
            {
                unique_lock<mutex> lk(mtx);
                cv.wait(lk, [&]
                        { return numDone == i - 1; });
                printFizzBuzz();
                numDone++;
                cv.notify_all();
            }
        }
    }

    // printNumber(x) outputs "x", where x is an integer.
    void number(function<void(int)> printNumber)
    {
        for (int i = 1; i <= n; i++)
        {
            if (i % 3 != 0 && i % 5 != 0)
            {
                unique_lock<mutex> lk(mtx);
                cv.wait(lk, [&]
                        { return numDone == i - 1; });
                printNumber(i);
                numDone++;
                cv.notify_all();
            }
        }
    }
};

1226. 哲学家进餐

5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

在这里插入图片描述

问题描述和图片来自维基百科 wikipedia.org

哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

philosopher 哲学家的编号。
pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。
eat 表示吃面。
putLeftFork 和 putRightFork 表示放下左边或右边的叉子。
由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。
给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

示例:

输入:n = 1
输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解释:
n 表示每个哲学家需要进餐的次数。
输出数组描述了叉子的控制和进餐的调用,它的格式如下:
output[i] = [a, b, c] (3个整数)

  • a 哲学家编号。
  • b 指定叉子:{1 : 左边, 2 : 右边}.
  • c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
    如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。

提示:

1 <= n <= 60

自己比较习惯使用信号量,第一反应是采用互斥锁+信号量来解决,如下:

class DiningPhilosophers
{
    bool fork[5] = {false, false, false, false, false};
    mutex mtx;
    condition_variable cv;

public:
    DiningPhilosophers()
    {
    }

    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork)
    {
        unique_lock<mutex> lk(mtx);
        cv.wait(lk, [&]
                { return !fork[philosopher] && !fork[(philosopher + 1) % 5]; });
        fork[philosopher] = true;
        fork[(philosopher + 1) % 5] = true;
        pickLeftFork();
        pickRightFork();
        // 拿完叉子后唤醒其他线程
        cv.notify_all();
        eat();
        putLeftFork();
        putRightFork();
        fork[philosopher] = false;
        fork[(philosopher + 1) % 5] = false;
        // 放下叉子后唤醒其他线程
        cv.notify_all();
    }
};

之后参考了题解1的做法,发现这道题目,用信号量,也就是PV操作,代码如下:

class DiningPhilosophers
{
    sem_t numAllow;
    sem_t fork[5];

public:
    DiningPhilosophers()
    {
        sem_init(&numAllow, 0, 4);
        for (int i = 0; i < 5; i++)
        {
            sem_init(&fork[i], 0, 1);
        }
    }

    void wantsToEat(int philosopher,
                    function<void()> pickLeftFork,
                    function<void()> pickRightFork,
                    function<void()> eat,
                    function<void()> putLeftFork,
                    function<void()> putRightFork)
    {
        // P操作
        sem_wait(&numAllow);
        sem_wait(&fork[philosopher]);
        sem_wait(&fork[(philosopher + 1) % 5]);
        // 吃饭
        pickLeftFork();
        pickRightFork();
        eat();
        putLeftFork();
        putRightFork();
        // V操作
        sem_post(&fork[philosopher]);
        sem_post(&fork[(philosopher + 1) % 5]);
        sem_post(&numAllow);
    }
};

  1. https://leetcode.cn/problems/the-dining-philosophers/solutions/2617719/xin-hao-liang-xian-zhi-tong-shi-jin-can-jqb95/ ↩︎

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cherries Man

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值