C++11 - thread多線程編程,線程互斥和同步通信,死鎖問題分析解決

C++11的多線程類thread
C++11之前,C++庫中沒有提供和線程相關的類或者接口,因此在編寫多線程程序時,Windows上需要調用CreateThread創建線程,Linux下需要調用clone或者pthread線程庫的接口函數pthread_create來創建線程。但是這樣是直接調用了系統相關的API函數,編寫的代碼,無法做到跨平臺編譯運行。

C++11之後提供了thread線程類,可以很方便的編寫多線程程序(注意:編譯器需要支持C++11之後的語法,推薦VS2017,g++4.6版本以上),代碼示例如下:

#include <iostream>
#include <thread>
#include <string>
using namespace std;

// 線程1的線程函數
void threadProc1()
{
    cout << "thread-1 run begin!" << endl;
    // 線程1睡眠2秒
    std::this_thread::sleep_for(std::chrono::seconds(2));
    cout << "thread-1 2秒喚醒,run end!" << endl;
}

// 線程2的線程函數
void threadProc2(int val, string info)
{
    cout << "thread-2 run begin!" << endl;
    cout << "thread-2 args[val:" << val << ",info:" << info << "]" << endl;
    // 線程2睡眠4秒
    std::this_thread::sleep_for(std::chrono::seconds(4));
    cout << "thread-2 4秒喚醒,run end!" << endl;
}
int main()
{
    cout << "main thread begin!" << endl;

    // 創建thread線程對象,傳入線程函數和參數,線程直接啓動運行
    thread t(threadProc1);
    thread t1(threadProc2, 20, "hello world");

    // 等待線程t和t1執行完,main線程再繼續運行
    t.join();
    t1.join();

    cout << "main thread end!" << endl;
    return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
代碼運行打印如下:

main thread begin!
thread-1 run begin!
thread-2 run begin!
thread-2 args[val:20,info:hello world]
thread-1 2秒喚醒,run end!
thread-2 4秒喚醒,run end!
main thread end!
1
2
3
4
5
6
7
可以看到,在C++語言層面編寫多線程程序,用thread線程類非常簡單,定義thread對象,只需要傳入相應的線程函數和參數就可以了。

上面同樣的代碼在Linux平臺下面用g++編譯:
g++ 源文件名字.cpp -lpthread
【注意】:需要鏈接pthread線程動態庫,所以C++的thread類在Linux環境下使用的就是pthread線程庫的相關接口。

然後用strace命令跟蹤程序的啓動過程:
tony@tony-virtual-machine:~/code$ strace ./a.out

有如下打印輸出:

說明C++ thread線程對象啓動線程的調用過程就是 thread->pthread_create->clone,還是Linux pthread線程庫使用的那一套,好處就是現在可以跨平臺編譯運行了,在Windows上當然調用的就是CreateThread系統API創建線程了。

線程互斥
在多線程環境中運行的代碼段,需要考慮是否存在競態條件,如果存在競態條件,我們就說該代碼段不是線程安全的,不能直接運行在多線程環境當中,對於這樣的代碼段,我們經常稱之爲臨界區資源,對於臨界區資源,多線程環境下需要保證它以原子操作執行,要保證臨界區的原子操作,就需要用到線程間的互斥操作-鎖機制,thread類庫還提供了更輕量級的基於CAS操作的原子操作類。

下面用模擬3個窗口同時賣票的場景,用代碼示例一下線程間的互斥操作。

thread線程類庫的互斥鎖mutex
下面這段代碼,啓動三個線程模擬三個窗口同時賣票,總票數是100張,由於整數的- -操作不是線程安全的操作,因爲多線程環境中,需要通過加互斥鎖做到線程安全,代碼如下示例:

// 車票總數是100張
volatile int tickets = 100; 
// 全局的互斥鎖
std::mutex mtx;

// 線程函數
void sellTicketTask(std::string wndName)
{
    while (tickets > 0)
    {
        // 獲取互斥鎖資源
        mtx.lock();
        if (tickets > 0)
        {
            std::cout << wndName << " 售賣第" << tickets << "張票" << std::endl;
            tickets--;
        }
        // 釋放互斥鎖資源
        mtx.unlock();

        // 每賣出一張票,睡眠100ms,讓每個窗口都有機會賣票
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

// 模擬車站窗口賣票,使用C++11 線程互斥鎖mutex
int main()
{
    // 創建三個模擬窗口賣票線程
    std::thread t1(sellTicketTask, "車票窗口一");
    std::thread t2(sellTicketTask, "車票窗口二");
    std::thread t3(sellTicketTask, "車票窗口三");

    // 等待三個線程執行完成
    t1.join();
    t2.join();
    t3.join();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
通過上面的代碼可以看到,C++11的mutex和Linux平臺下pthread線程庫的pthread_mutex_t互斥鎖使用幾乎是一樣的(實際上在Linux平臺下mutex就是調用的pthread_mutex_t互斥鎖相關的系統函數),mutex也支持trylock活鎖機制,可以自己進行測試。

thread線程類庫基於CAS的原子類
實際上,上面代碼中因爲tickets車票數量是整數,因此它的- -操作需要在多線程環境下添加互斥操作,但是mutex互斥鎖畢竟比較重,對於系統消耗有些大,C++11的thread類庫提供了針對簡單類型的原子操作類,如std::atomic_int,atomic_long,atomic_bool等,它們值的增減都是基於CAS操作的,既保證了線程安全,效率還非常高。

下面代碼示例開啓10個線程,每個線程對整數增加1000次,保證線程安全的情況下,應該加到10000次,這種情況下,可以用atomic_int來實現,代碼示例如下:

#include <iostream>
#include <atomic> // C++11線程庫提供的原子類
#include <thread> // C++線程類庫的頭文件
#include <vector>

// 原子整形,CAS操作保證給count自增自減的原子操作
std::atomic_int count = 0;

// 線程函數
void sumTask()
{
    // 每個線程給count加1000次
    for (int i = 0; i < 1000; ++i)
    {
        count++;
    }
}

int main()
{
    // 創建10個線程放在容器當中
    std::vector<std::thread> vec;
    for (int i = 0; i < 10; ++i)
    {
        vec.push_back(std::thread(sumTask));
    }

    // 等待線程執行完成
    for (int i = 0; i < vec.size(); ++i)
    {
        vec[i].join();
    }

    // 所有子線程運行結束,count的結果每次運行應該都是10000
    std::cout << "count : " << count << std::endl;

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
實際上,C++11類庫的原子操作類,在Linux平臺下調用的也是CAS(compare_and_set)相關的系統接口。

線程同步通信
多線程在運行過程中,各個線程都是隨着OS的調度算法,佔用CPU時間片來執行指令做事情,每個線程的運行完全沒有順序可言。但是在某些應用場景下,一個線程需要等待另外一個線程的運行結果,才能繼續往下執行,這就需要涉及線程之間的同步通信機制。

線程間同步通信最典型的例子就是生產者-消費者模型,生產者線程生產出產品以後,會通知消費者線程去消費產品;如果消費者線程去消費產品,發現還沒有產品生產出來,它需要通知生產者線程趕快生產產品,等生產者線程生產出產品以後,消費者線程才能繼續往下執行。

C++11 線程庫提供的條件變量condition_variable,就是Linux平臺下的Condition Variable機制,用於解決線程間的同步通信問題,下面通過代碼演示一個生產者-消費者線程模型,仔細分析代碼:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 定義互斥鎖(條件變量需要和互斥鎖一起使用)
std::mutex mtx;
// 定義條件變量(用來做線程間的同步通信)
std::condition_variable cv;
// 定義vector容器,作爲生產者和消費者共享的容器
std::vector<int> vec;

// 生產者線程函數
void producer()
{
    // 生產者每生產一個,就通知消費者消費一個
    for (int i = 1; i <= 10; ++i)
    {
        // 獲取mtx互斥鎖資源
        std::unique_lock<std::mutex> lock(mtx);

        // 如果容器不爲空,代表還有產品未消費,等待消費者線程消費完,再生產
        while (!vec.empty())
        {
            // 判斷容器不爲空,進入等待條件變量的狀態,釋放mtx鎖,
            // 讓消費者線程搶到鎖能夠去消費產品
            cv.wait(lock);
        }
        vec.push_back(i); // 表示生產者生產的產品序號i
        std::cout << "producer生產產品:" << i << std::endl;

        /* 
        生產者線程生產完產品,通知等待在cv條件變量上的消費者線程,
        可以開始消費產品了,然後釋放鎖mtx
        */
        cv.notify_all();

        // 生產一個產品,睡眠100ms
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
// 消費者線程函數
void consumer()
{
    // 消費者每消費一個,就通知生產者生產一個
    for (int i = 1; i <= 10; ++i)
    {
        // 獲取mtx互斥鎖資源
        std::unique_lock<std::mutex> lock(mtx);

        // 如果容器爲空,代表還有沒有產品可消費,等待生產者生產,再消費
        while (vec.empty())
        {
            // 判斷容器爲空,進入等待條件變量的狀態,釋放mtx鎖,
            // 讓生產者線程搶到鎖能夠去生產產品
            cv.wait(lock);
        }
        int data = vec.back(); // 表示消費者消費的產品序號i
        vec.pop_back();
        std::cout << "consumer消費產品:" << data << std::endl;

        /*
        消費者消費完產品,通知等待在cv條件變量上的生產者線程,
        可以開始生產產品了,然後釋放鎖mtx
        */
        cv.notify_all();

        // 消費一個產品,睡眠100ms
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
int main()
{
    // 創建生產者和消費者線程
    std::thread t1(producer);
    std::thread t2(consumer);

    // main主線程等待所有子線程執行完
    t1.join();
    t2.join();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
代碼運行結果如下,可以看到,生產者和消費者線程交替生產產品和消費產品,兩個線程之間進行了完美的通信協調運行。

producer生產產品:1
consumer消費產品:1
producer生產產品:2
consumer消費產品:2
producer生產產品:3
consumer消費產品:3
producer生產產品:4
consumer消費產品:4
producer生產產品:5
consumer消費產品:5
producer生產產品:6
consumer消費產品:6
producer生產產品:7
consumer消費產品:7
producer生產產品:8
consumer消費產品:8
producer生產產品:9
consumer消費產品:9
producer生產產品:10
consumer消費產品:10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
死鎖問題案例分析解決
死鎖的問題經常會考察到,面對哪些情況下會程序會發生死鎖的問題,與其想着怎麼把書上的理論背出來,不如從實踐的角度舉例說明,如何對死鎖的問題進行分析定位,然後找到問題點進行修改。

當我們的程序運行時,出現假死的現象,有可能是程序死循環了,有可能是程序等待的I/O、網絡事件沒發生導致程序阻塞了,也有可能是程序死鎖了,下面舉例說明在Linux系統下如何分許我們程序的死鎖問題。

示例:
當一個程序的多個線程獲取多個互斥鎖資源的時候,就有可能發生死鎖問題,比如線程A先獲取了鎖1,線程B獲取了鎖2,進而線程A還需要獲取鎖2才能繼續執行,但是由於鎖2被線程B持有還沒有釋放,線程A爲了等待鎖2資源就阻塞了;線程B這時候需要獲取鎖1才能往下執行,但是由於鎖1被線程A持有,導致A也進入阻塞。

線程A和線程B都在等待對方釋放鎖資源,但是它們又不肯釋放原來的鎖資源,導致線程A和B一直互相等待,進程死鎖了。下面代碼示例演示這個問題:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 鎖資源1
std::mutex mtx1;
// 鎖資源2
std::mutex mtx2;

// 線程A的函數
void taskA()
{
    // 保證線程A先獲取鎖1
    std::lock_guard<std::mutex> lockA(mtx1);
    std::cout << "線程A獲取鎖1" << std::endl;

    // 線程A睡眠2s再獲取鎖2,保證鎖2先被線程B獲取,模擬死鎖問題的發生
    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 線程A先獲取鎖2
    std::lock_guard<std::mutex> lockB(mtx2);
    std::cout << "線程A獲取鎖2" << std::endl;

    std::cout << "線程A釋放所有鎖資源,結束運行!" << std::endl;
}

// 線程B的函數
void taskB()
{
    // 線程B先睡眠1s保證線程A先獲取鎖1
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::lock_guard<std::mutex> lockB(mtx2);
    std::cout << "線程B獲取鎖2" << std::endl;

    // 線程B嘗試獲取鎖1
    std::lock_guard<std::mutex> lockA(mtx1);
    std::cout << "線程B獲取鎖1" << std::endl;

    std::cout << "線程B釋放所有鎖資源,結束運行!" << std::endl;
}
int main()
{
    // 創建生產者和消費者線程
    std::thread t1(taskA);
    std::thread t2(taskB);

    // main主線程等待所有子線程執行完
    t1.join();
    t2.join();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
運行上面的程序,打印如下:

tony@tony-virtual-machine:~/code$ ./a.out 
線程A獲取鎖1
線程B獲取鎖2
1
2
3
可以看到,線程A獲取鎖1、線程B獲取鎖2以後,進程就不往下繼續執行了,一直等待在這裏,如果這是我們碰到的一個問題場景,我們如何判斷出這是由於線程間死鎖引起的呢?

先通過ps命令查看一下進程當前的運行狀態和PID,如下:
root@tony-virtual-machine:/home/tony# ps -aux | grep a.out
tony 1953 0.0 0.0 98108 1904 pts/0 Sl+ 10:41 0:00 ./a.out
root 2064 0.0 0.0 21536 1076 pts/1 S+ 10:51 0:00 grep --color=auto a.out
從上面的命令可以看出,a.out進程的PID是1953,當前狀態是Sl+,相當於是多線程程序,全部進入阻塞狀態。

通過top命令再查看一下進程內每個線程具體的運行情況,如下:
root@tony-virtual-machine:/home/tony# top -Hp 1953

進程 USER PR NI VIRT RES SHR CPU %MEM TIME+ COMMAND
1953 tony 20 0 98108 1904 1752 S 0.0 0.1 0:00.00 a.out
1954 tony 20 0 98108 1904 1752 S 0.0 0.1 0:00.00 a.out
1955 tony 20 0 98108 1904 1752 S 0.0 0.1 0:00.00 a.out

從top命令的打印信息可以看出,所有線程都進入阻塞狀態,CPU佔用率都爲0.0,可以排除是死循環的問題,因爲死循環會造成CPU使用率居高不下,而且線程的狀態也不會是S。那麼接下來有可能是由於I/O網絡事件沒有發生使線程阻塞,或者是線程發生死鎖問題了。

通過gdb遠程調試正在運行的程序,打印進程每一個線程的調用堆棧信息,過程如下:
通過gdb attach pid遠程調試上面的a.out進程,命令如下:
root@tony-virtual-machine:/home/tony# gdb attach 1953

進入gdb調試命令行以後,打印所有線程的調用棧信息,信息如下:
(gdb) thread apply all bt

Thread 3 (Thread 0x7feb523ec700 (LWP 1955)):
#0 _llllock_wait () at …/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007feb53928023 in __GI___pthread_mutex_lock (mutex=0x5646aabe7140 ) at …/nptl/pthread_mutex_lock.c:78
#2 0x00005646aa9e40bf in __gthread_mutex_lock(pthread_mutex_t*) ()
#3 0x00005646aa9e4630 in std::mutex::lock() ()
#4 0x00005646aa9e46ac in std::lock_guardstd::mutex::lock_guard(std::mutex&) ()
#5 0x00005646aa9e42c0 in taskB() ()
#6 0x00005646aa9e4bdb in void std::__invoke_impl<void, void ()()>(std::__invoke_other, void (&&)()) ()
#7 0x00005646aa9e49e8 in std::__invoke_result<void ()()>::type std::__invoke<void ()()>(void (&&)()) ()
#8 0x00005646aa9e50b6 in decltype (__invoke((_S_declval<0ul>)())) std::thread::_Invoker<std::tuple<void ()()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#9 0x00005646aa9e5072 in std::thread::_Invoker<std::tuple<void ()()> >::operator()() ()
#10 0x00005646aa9e5042 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void ()()> > >::_M_run() ()
#11 0x00007feb5365257f in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007feb539256db in start_thread (arg=0x7feb523ec700) at pthread_create.c:463
#13 0x00007feb530ad88f in clone () at …/sysdeps/unix/sysv/linux/x86_64/clone.S:95

Thread 2 (Thread 0x7feb52bed700 (LWP 1954)):
#0 _llllock_wait () at …/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1 0x00007feb53928023 in __GI___pthread_mutex_lock (mutex=0x5646aabe7180 ) at …/nptl/pthread_mutex_lock.c:78
#2 0x00005646aa9e40bf in __gthread_mutex_lock(pthread_mutex_t*) ()
#3 0x00005646aa9e4630 in std::mutex::lock() ()
#4 0x00005646aa9e46ac in std::lock_guardstd::mutex::lock_guard(std::mutex&) ()
#5 0x00005646aa9e4183 in taskA() ()
#6 0x00005646aa9e4bdb in void std::__invoke_impl<void, void ()()>(std::__invoke_other, void (&&)()) ()
#7 0x00005646aa9e49e8 in std::__invoke_result<void ()()>::type std::__invoke<void ()()>(void (&&)()) ()
#8 0x00005646aa9e50b6 in decltype (__invoke((_S_declval<0ul>)())) std::thread::_Invoker<std::tuple<void ()()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#9 0x00005646aa9e5072 in std::thread::_Invoker<std::tuple<void ()()> >::operator()() ()
#10 0x00005646aa9e5042 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void ()()> > >::_M_run() ()
#11 0x00007feb5365257f in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007feb539256db in start_thread (arg=0x7feb52bed700) at pthread_create.c:463
#13 0x00007feb530ad88f in clone () at …/sysdeps/unix/sysv/linux/x86_64/clone.S:95

Thread 1 (Thread 0x7feb53d4b740 (LWP 1953)):
—Type to continue, or q to quit—
#0 0x00007feb53926d2d in __GI___pthread_timedjoin_ex (threadid=140648682280704, thread_return=0x0, abstime=0x0,
block=) at pthread_join_common.c:89
#1 0x00007feb536527d3 in std::thread::join() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#2 0x00005646aa9e43bb in main ()
(gdb)

從上面的線程調用棧信息可以看到,當前進程有三個線程,分別是Thread1是main線程,Thread2是taskA線程,Thread3是taskB線程。

從調用棧信息可以看到,Thread3線程進入S阻塞狀態的原因是因爲它最後在#0 _llllock_wait () at,也就是它在等待獲取一把鎖(lock_wait),而且堆棧信息打印的很清晰,#1 0x00007feb53928023 in __GI___pthread_mutex_lock (mutex=0x5646aabe7140 ) at …/nptl/pthread_mutex_lock.c:78,Thread3在獲取而獲取不到,因此進入阻塞狀態了。這裏結合代碼分析,Thread3線程(也就是taskB)最後在這裏阻塞了:
void taskB()
{
// 線程B先睡眠1s保證線程A先獲取鎖1
std::this_thread::sleep_for(std::chrono::seconds(1));
std::lock_guardstd::mutex lockB(mtx2);
std::cout << “線程B獲取鎖2” << std::endl;
// 線程B嘗試獲取鎖1
std::lock_guardstd::mutex lockA(mtx1); ===》 這裏阻塞了!如果不知道怎麼定位到源代碼行上,看下一小節!
std::cout << “線程B獲取鎖1” << std::endl;
std::cout << “線程B釋放所有鎖資源,結束運行!” << std::endl;
}

依然是從調用棧信息可以看到,Thread2線程進入S阻塞狀態的原因是因爲它最後在#0 _llllock_wait () at,也就是它在等待獲取一把鎖(lock_wait),而且堆棧信息打印的很清晰,#1 0x00007feb53928023 in __GI___pthread_mutex_lock (mutex=0x5646aabe7180 ) at …/nptl/pthread_mutex_lock.c:78,Thread2在獲取而獲取不到,因此進入阻塞狀態了。這裏結合代碼分析,Thread2線程(也就是taskA)最後在這裏阻塞了:
void taskA()
{
// 保證線程A先獲取鎖1
std::lock_guardstd::mutex lockA(mtx1);
std::cout << “線程A獲取鎖1” << std::endl;
// 線程A睡眠2s再獲取鎖2,保證鎖2先被線程B獲取,模擬死鎖問題的發生
std::this_thread::sleep_for(std::chrono::seconds(2));
// 線程A先獲取鎖2
std::lock_guardstd::mutex lockB(mtx2); ===》 這裏阻塞了!如果不知道怎麼定位到源代碼行上,看下一小節!
std::cout << “線程A獲取鎖2” << std::endl;
std::cout << “線程A釋放所有鎖資源,結束運行!” << std::endl;
}

既然定位到taskA和taskB線程阻塞的原因,都是因爲鎖獲取不到,然後再結合源碼進行分析定位,最終發現taskA之所以獲取不到mtx2,是因爲mtx2早被taskB線程獲取了;同樣taskB之所以獲取不到mtx1,是因爲mtx1早被taskA線程獲取了,導致所有線程進入阻塞狀態,等待鎖資源的獲取,但是又因爲沒有線程釋放鎖,最終導致死鎖問題。(從各線程調用棧信息能看出來,這裏面和I/O網絡事件沒什麼關係)

怎麼在源碼上定位到問題代碼
實際上,上面的代碼運行一般是發佈後的release版本,內部沒有調試信息,我們如果想把死鎖的原因定位到源碼的某一行代碼上,就需要一個debug版本(g++編譯添加-g選項),操作如下:
1.編譯命令
tony@tony-virtual-machine:~/code$ g++ 20190316.cpp -g -lpthread
2. 運行代碼
tony@tony-virtual-machine:~/code$ ./a.out
線程A獲取鎖1
線程B獲取鎖2
…(程序到這裏不往下運行了)
3.gdb調試該進程
root@tony-virtual-machine:/home/tony/code# ps -ef | grep a.out
tony 2617 1535 0 12:32 pts/0 00:00:00 ./a.out
root@tony-virtual-machine:/home/tony/code# gdb attach 2617
4.查看當前所有的線程
(gdb) info threads

  Id   Target Id         Frame 
* 1    Thread 0x7f8c63002740 (LWP 2617) "a.out" 0x00007f8c62bddd2d in __GI___pthread_timedjoin_ex (
    threadid=140240914892544, thread_return=0x0, abstime=0x0, block=<optimized out>) at pthread_join_common.c:89
  2    Thread 0x7f8c61ea4700 (LWP 2618) "a.out" __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
  3    Thread 0x7f8c616a3700 (LWP 2619) "a.out" __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
1
2
3
4
5
可以看到有三個線程。

5.切換到線程2
(gdb) thread 2
6.查看線程2目前的調用棧信息,where或者bt命令都可以
(gdb) where

(gdb) where
#0  __lll_lock_wait () at ../sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:135
#1  0x00007f8c62bdf023 in __GI___pthread_mutex_lock (mutex=0x55678928e180 <mtx2>) at ../nptl/pthread_mutex_lock.c:78
#2  0x000055678908b0bf in __gthread_mutex_lock (__mutex=0x55678928e180 <mtx2>)
    at /usr/include/x86_64-linux-gnu/c++/7/bits/gthr-default.h:748
#3  0x000055678908b630 in std::mutex::lock (this=0x55678928e180 <mtx2>) at /usr/include/c++/7/bits/std_mutex.h:103
#4  0x000055678908b6ac in std::lock_guard<std::mutex>::lock_guard (this=0x7f8c61ea3dc0, __m=...)
    at /usr/include/c++/7/bits/std_mutex.h:162
#5  0x000055678908b183 in taskA () at 20190316.cpp:23
#6  0x000055678908bbdb in std::__invoke_impl<void, void (*)()> (__f=@0x556789d78e78: 0x55678908b0f7 <taskA()>)
    at /usr/include/c++/7/bits/invoke.h:60
#7  0x000055678908b9e8 in std::__invoke<void (*)()> (__fn=@0x556789d78e78: 0x55678908b0f7 <taskA()>)
    at /usr/include/c++/7/bits/invoke.h:95
#8  0x000055678908c0b6 in std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul> (this=0x556789d78e78)
    at /usr/include/c++/7/thread:234
#9  0x000055678908c072 in std::thread::_Invoker<std::tuple<void (*)()> >::operator() (this=0x556789d78e78)
    at /usr/include/c++/7/thread:243
#10 0x000055678908c042 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run (
    this=0x556789d78e70) at /usr/include/c++/7/thread:186
#11 0x00007f8c6290957f in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#12 0x00007f8c62bdc6db in start_thread (arg=0x7f8c61ea4700) at pthread_create.c:463
#13 0x00007f8c6236488f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
7.查看上面線程2的第5幀信息#5 0x000055678908b183 in taskA () at 20190316.cpp:23
(gdb) f 5
#5 0x000055678908b183 in taskA () at 20190316.cpp:23
23 std::lock_guard< std::mutex > lockB(mtx2);
可以看到,這裏就直接定位到代碼一直阻塞在了20190316.cpp的第23行,對應的行代碼是std::lock_guard< std::mutex > lockB(mtx2);

可以同樣的步驟定位查看線程3的問題代碼行。

死鎖問題代碼修改
既然發現了問題,那麼就知道這個問題場景發生死鎖,是由於多個線程獲取多個鎖資源的時候,順序不一致導致的死鎖問題,那麼保證它們獲取鎖的順序是一致的,問題就可以解決,代碼修改如下:

#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
#include <vector>

// 鎖資源1
std::mutex mtx1;
// 鎖資源2
std::mutex mtx2;

// 線程A的函數
void taskA()
{
    // 保證線程A先獲取鎖1
    std::lock_guard<std::mutex> lockA(mtx1);
    std::cout << "線程A獲取鎖1" << std::endl;

    // 線程A嘗試獲取鎖2
    std::lock_guard<std::mutex> lockB(mtx2);
    std::cout << "線程A獲取鎖2" << std::endl;

    std::cout << "線程A釋放所有鎖資源,結束運行!" << std::endl;
}

// 線程B的函數
void taskB()
{
    // 線程B獲取鎖1
    std::lock_guard<std::mutex> lockA(mtx1);
    std::cout << "線程B獲取鎖1" << std::endl;

    // 線程B嘗試獲取鎖2
    std::lock_guard<std::mutex> lockB(mtx2);
    std::cout << "線程B獲取鎖2" << std::endl;

    std::cout << "線程B釋放所有鎖資源,結束運行!" << std::endl;
}
int main()
{
    // 創建生產者和消費者線程
    std::thread t1(taskA);
    std::thread t2(taskB);

    // main主線程等待所有子線程執行完
    t1.join();
    t2.join();

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
程序運行正常,打印如下:

線程A獲取鎖1
線程A獲取鎖2
線程A釋放所有鎖資源,結束運行!
線程B獲取鎖1
線程B獲取鎖2
線程B釋放所有鎖資源,結束運行!
1
2
3
4
5
6
【注意】:不做要書呆了,任何問題都要從實踐的角度去考慮問題如何定位分析解決,理論結合實踐!
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章