iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0

除了死鎖,還有其他可能會讓程式卡住,一個是「活鎖(livelock)」、另一個是「飢餓(starvation)」。

活鎖 Livelock

有別於「死鎖」是複數個執行緒都持有部分資源、等待其他執行緒釋出另一部分的資源而卡死,「活鎖」則是複數個執行緒都互相釋出資源,導致彼此都沒有足夠的資源完成工作,一樣會卡著無法運行。

下列程式碼即為「活鎖」範例:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex spoon1, spoon2; // 各自命名為 spoon1 與 spoon2 的互斥鎖

void worker(const char* name, std::mutex& first, std::mutex& second) {
    while (true) {
        first.lock();   // 執行緒先拿第一把湯匙
        std::cout << name << " got first spoon\n";
        
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));  // 等待一秒鐘

        if (!second.try_lock()) {    // 如果拿不到另一把湯匙
            std::cout << name << " gives up to be polite\n";
            first.unlock();          // 就把手上的湯匙給放出來
            continue;
        }
        
        // 若有一執行緒順利拿到兩把湯匙,就執行這裡
        std::cout << name << " got both spoons and can eat\n";
        second.unlock();
        first.unlock();
        return;
    }

    std::cout << name << " failed after many polite retries\n";
}

int main() {
    std::thread t1(worker, "A", std::ref(spoon1), std::ref(spoon2));
    std::thread t2(worker, "B", std::ref(spoon2), std::ref(spoon1));
    t1.join();
    t2.join();
}

在此範例中,第一個執行緒、也就是取名為 Aworker 先取 spoon1 再取 spoon2;第二個執行緒、也就是取名為 Bworker 先取 spoon2 再取 spoon1

對於兩個執行緒來說,都想要拿到兩把湯匙才可以吃飯,但在 if (!second.try_lock()) 階段,會看如果拿不到另一把湯匙,就把手上的湯匙給放出來,但兩執行緒互相讓來讓去,最後都無法完整持有兩把湯匙,也就無法吃飯。

這樣的程式碼執行結果如下:

A got first spoon
B got first spoon
A gives up to be polite
A got first spoon
B gives up to be polite
...

可發現兩執行緒都能取得第一把湯匙,但拿到之後又一直把手上湯匙讓出來,因此無法順利拿到兩把湯匙、進到 std::cout << name << " got both spoons and can eat\n"; 區塊。

活鎖解法

Day 26 講死結的鎖法,從 C++ 17 開始可用 std::scoped_lock 協調,將所有的鎖統一管理,並透過其內建機制,確保不發生活鎖:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex spoon1, spoon2; // 各自命名為 spoon1 與 spoon2 的互斥鎖

void worker(const char* name, std::mutex& first, std::mutex& second) {
    // 把 first 與 second 都交給 scoped_lock 管理
    std::scoped_lock lock(first, second); 

    // 兩執行緒皆可順利抵達這行
    std::cout << name << " got both spoons and can eat\n";
    
    return;
}

int main() {
    std::thread t1(worker, "A", std::ref(spoon1), std::ref(spoon2));
    std::thread t2(worker, "B", std::ref(spoon2), std::ref(spoon1));
    t1.join();
    t2.join();
}

飢餓 Starvation

「飢餓」則是指某一執行緒一直得不到資源,只能在旁邊等,例如以下範例:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <string>

std::mutex resourceMutex;  // 名為 resourceMutex 的互斥鎖
bool isRunning = true;

void worker(std::string name, int holdTimeMs, int restTimeMs, int& successCount) {
    while (isRunning) {
        // 排隊取鎖並上鎖
        resourceMutex.lock();
        
        // 成功取得資源,開始工作
        successCount++;
        std::cout << name << " Got the lock " << successCount << " times.\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(holdTimeMs));
        
        // 釋放鎖
        resourceMutex.unlock();
        
        // 釋放鎖後的喘息時間
        if (restTimeMs > 0) {
            std::this_thread::sleep_for(std::chrono::milliseconds(restTimeMs));
        }
    }
}

int main() {
    int greedyCount = 0;
    int starvedCount = 0;

    std::cout << "Start\n";

    // 貪婪工人:拿到鎖後霸佔 500 毫秒,釋放後「完全不休息 (0ms)」立刻又想去排隊
    std::thread t1(worker, "Greedy worker", 500, 0, std::ref(greedyCount));
    
    // 可憐工人:拿到鎖後只用 10 毫秒,釋放後還要休息 10 毫秒
    std::thread t2(worker, "Poor worker", 10, 10, std::ref(starvedCount));

    // 讓遊戲跑 5 秒鐘後停止
    std::this_thread::sleep_for(std::chrono::seconds(5));
    isRunning = false;

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

    std::cout << "\n============= Final Result =============\n";
    std::cout << "Greedy worker got the lock " << greedyCount << " times.\n";
    std::cout << "Poor workder got the lock " << starvedCount << " times.\n";
    
    return 0;
}

這裡的「貪婪工人 t1」每次拿到鎖就霸佔長達 500 毫秒,且釋放後完全不休息就又去搶鎖;「可憐工人 t2」拿到鎖後只能用 10 毫秒,釋放後還再休息 10 豪秒,導致鎖長時間被「貪婪工人 t1」佔據,執行結果如下:

Start
Greedy worker Got the lock 1 times.
Greedy worker Got the lock 2 times.
Greedy worker Got the lock 3 times.
Greedy worker Got the lock 4 times.
Greedy worker Got the lock 5 times.
Greedy worker Got the lock 6 times.
Greedy worker Got the lock 7 times.
Greedy worker Got the lock 8 times.
Greedy worker Got the lock 9 times.
Greedy worker Got the lock 10 times.
Poor worker Got the lock 1 times.

============= Final Result =============
Greedy worker got the lock 10 times.
Poor workder got the lock 1 times.

跑了 5 秒鐘,「貪婪工人 t1」搶到 10 次、「可憐工人 t2」卻只拿到 1 次,我們說「可憐工人 t2」面臨拿不到資源的飢餓情況。

飢餓解法

飢餓的解法很多,以本例而言,最簡單的方法是讓「貪婪工人 t1」在釋放鎖後,也加上休息時間,這邊以休息 10 毫秒計算:

std::thread t1(worker, "Greedy worker", 500, 10, std::ref(greedyCount));

這樣鎖就不會永遠被「貪婪工人 t1」霸佔了,執行結果如下:

Start
Greedy worker Got the lock 1 times.
Poor worker Got the lock 1 times.
Greedy worker Got the lock 2 times.
Poor worker Got the lock 2 times.
Greedy worker Got the lock 3 times.
Poor worker Got the lock 3 times.
Greedy worker Got the lock 4 times.
Poor worker Got the lock 4 times.
Greedy worker Got the lock 5 times.
Poor worker Got the lock 5 times.
Greedy worker Got the lock 6 times.
Poor worker Got the lock 6 times.
Greedy worker Got the lock 7 times.
Poor worker Got the lock 7 times.
Greedy worker Got the lock 8 times.
Poor worker Got the lock 8 times.
Greedy worker Got the lock 9 times.
Poor worker Got the lock 9 times.
Greedy worker Got the lock 10 times.
Poor worker Got the lock 10 times.

============= Final Result =============
Greedy worker got the lock 10 times.
Poor workder got the lock 10 times.

兩工人各自都成功取得鎖 10 次。

參考資料

  1. Starvation and Livelock (GeeksforGeeks)

上一篇
Day 26-死結與解決方式
下一篇
Day 28-記憶體模型
系列文
韌體工程師的不只 0x10 個問題31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言