如何调试多线程程序

当然,多线程调试的前提是你需要熟悉多线程的基础知识,包括线程的创建和退出、线程之间的各种同步原语等。如果您还不熟悉多线程编程的内容,可以参考这个专栏《C++ 多线程编程专栏》,如果您不熟悉 gdb 调试可以参考这个专栏《Linux GDB 调试教程》。

一、调试多线程的方法

使用 gdb 将程序跑起来,然后按 Ctrl + C 将程序中断下来,使用 info threads 命令查看当前进程有多少线程。

如何调试多线程程序

还是以 redis-server 为例,当使用 gdb 将程序运行起来后,我们按 Ctrl + C 将程序中断下来,此时可以使用 info threads 命令查看 redis-server 有多少线程,每个线程正在执行哪里的代码。

使用 thread 线程编号 可以切换到对应的线程去,然后使用 bt 命令可以查看对应线程从顶到底层的函数调用,以及上层调用下层对应的源码中的位置;当然,你也可以使用 frame 栈函数编号 (栈函数编号即下图中的 #0 ~ #4,使用 frame 命令时不需要加 #)切换到当前函数调用堆栈的任何一层函数调用中去,然后分析该函数执行逻辑,使用 print 等命令输出各种变量和表达式值,或者进行单步调试。

如何调试多线程程序

如上图所示,我们切换到了 redis-server 的 1 号线程,然后输入 bt 命令查看该线程的调用堆栈,发现顶层是 main 函数,说明这是主线程,同时得到从 main 开始往下各个函数调用对应的源码位置,我们可以通过这些源码位置来学习研究调用处的逻辑。 对每个线程都进行这样的分析之后,我们基本上就可以搞清楚整个程序运行中的执行逻辑了。

接着我们分别通过得到的各个线程的线程函数名去源码中搜索,找到创建这些线程的函数(下文为了叙述方便,以 f 代称这个函数),再接着通过搜索 f 或者给 f 加断点重启程序看函数 f 是如何被调用的,这些操作一般在程序初始化阶段。

redis-server 1 号线线程是在 main 函数中创建的,我们再看下 2 号线程的创建,使用 thread 2 切换到 2号线程,然后使用 bt 命令查看 2 号线程的调用堆栈,得到 2 号线程的线程函数为 bioProcessBackgroundJobs ,注意在顶层的 clone 和 start_thread 是系统函数,我们找的线程函数应该是项目中的自定义线程函数。

如何调试多线程程序

通过在项目中搜索 bioProcessBackgroundJobs 函数,我们发现 bioProcessBackgroundJobs 函数在 bioInit 中被调用,而且确实是在 bioInit 函数中创建了线程 2,因此我们看到了 pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) 这样的调用。

<code> 

1

//bio.c

96

2void

bioInit(void)

{

3

//...省略部分代码...

4

5

for

(j

=

0

;

j

<

BIO_NUM_OPS;

j++)

{

6

void

*arg

=

(void*)(unsigned

long)

j;

7

//在这里创建了线程

bioProcessBackgroundJobs

8

if

(pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg)

!=

0

)

{

9

serverLog(LL_WARNING,"Fatal:

Can't

initialize

Background

Jobs.");

10

exit(1);

11

}

12

bio_threads[j]

=

thread;

13

}

14

}

/<code>

此时,我们可以继续在项目中查找 bioInit 函数,看看它在哪里被调用的,或者直接给 bioInit 函数加上断点,然后重启 redis-server,等断点触发,使用 bt 命令查看此时的调用堆栈就知道 bioInit 函数在何处调用的了。

<code> 

1

(gdb)

b

bioInit

2Breakpoint

1

at

0x498e5e

:

file

bio

.c

,

line

103

.

3

(gdb)

r

4The

program

being

debugged

has

been

started

already

.

5Start

it

from

the

beginning

? (y or n)

y

6Starting

program

: /

root

/

redis-6

.0

.3

/

src

/

redis-server

7

[Thread debugging using libthread_db enabled]

8

9Breakpoint

1

,

bioInit

()

at

bio

.c

:103

10103

for

(j =

0

; j < BIO_NUM_OPS; j++) {

11

(gdb)

bt

12

#0

bioInit

()

at

bio

.c

:103

13

#1

0x0000000000431b5d

in

InitServerLast

()

at

server

.c

:2953

14

#2

0x000000000043724f

in

main

(argc=

1

, argv=

0

x7fffffffe318)

at

server

.c

:5142

15

(gdb) /<code>

至此我们发现 2 号线程是在 main 函数中调用了 InitServerLast 函数,后者又调用 bioInit 函数,然后在 bioInit 函数中创建了新的线程 bioProcessBackgroundJobs ,我们只要分析这个执行流就能搞清楚这个逻辑流程了。

同样的道理,redis-server 还有 3 号和 4 号线程,我们也可以按分析 2 号线程的方式去分析 3 号和 4号,读者可以按照这里介绍的方法。

以上就是我阅读一个不熟悉的 C/C++ 项目常用的方法,当然对于一些特殊的项目的源码,你还需要去了解一下该项目的的业务内容,否则除了技术逻辑以外,你可能需要一些业务知识才能看懂各个线程调用栈以及初始化各个线程函数过程中的业务逻辑。

二、调试时控制线程切换

在调试多线程程序时,有时候我们希望执行流一直在某个线程执行,而不是切换到其他线程,有办法做到这样吗?

为了说明清楚这个问题,我们假设现在调试的程序有 5 个线程,除了主线程,其他 4 个工作线程的线程函数都是下面这样一个函数:

<code> 

1

void

*

worker_thread_proc

(

void

* arg)

2

{

3

while

(

true

)

4

{

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

}

21

} /<code>

为了方便表述,我们把四个工作线程分别叫做 AB

CD

如何调试多线程程序

如上图所示,假设某个时刻, 线程 A 的停在 代码行 3 处 ,线程 B、C、D 停留位置代码行 1 ~15 任一位置,此时线程 A 是 gdb 当前调试线程,此时我们输入 next 命令,期望调试器跳转到 代码行 4 处;或者输入 util 10

命令,期望调试器跳转到**代码行 10 **处。但是实际情况下,如果 代码行 1代码行 2代码行 13 或者 代码行 14 处 设置了断点,gdb 再次停下来的时候,可能会停在到 代码行 1代码行 2代码行 13代码行 14 这样的地方。

这是多线程程序的特点:当我们从 代码行 4 处让程序继续运行时,线程 A 虽然会继续往下执行,下一次应该在 代码行 14 处停下来,但是线程 BCD 也在同步运行呀,如果此时系统的线程调度将 CPU 时间片切换到线程 B

C 或者 D 呢?那么 gdb 最终停下来的时候,可能是线程 BCD 触发了 代码行 1代码行 2代码行 13代码行 14 处的断点,此时调试的线程会变为 BC 或者 D ,而此时打印相关的变量值,可能就不是我们期望的线程 A 函数中的相关变量值了。

还存在一个情况,我们单步调试线程 A 时,我们不希望线程 A 函数中的值被其他线程改变。

针对调试多线程存在的上述状况,gdb 提供了一个在调试时将程序执行流锁定在当前调试线程的命令选项——

scheduler-locking 选项,这个选项有三个值,分别是 on、step 和 off,使用方法如下:

<code>

1set

scheduler-locking

on

/step/

off

/<code>

set scheduler-locking on可以用来锁定当前线程,只观察这个线程的运行情况, 当锁定这个线程时, 其他线程就处于了暂停状态,也就是说你在当前线程执行 next、step、until、finish、return 命令时,其他线程是不会运行的。

需要注意的是,你在使用 set scheduler-locking on/step 选项时要确认下当前线程是否是你期望锁定的线程,如果不是,可以使用 thread + 线程编号 切换到你需要的线程再调用 set scheduler-locking on/step 进行锁定。

set scheduler-locking step也是用来锁定当前线程,当且仅当使用 next 或 step 命令做单步调试时会锁定当前线程,如果你使用 until、finish、return 等线程内调试命令,但是它们不是单步命令,所以其他线程还是有机会运行的。相比较 on 选项值,step 选项值给为单步调试提供了更加精细化的控制,因为通常我们只希望在单步调试时,不希望其他线程对当前调试的各个变量值造成影响。

set scheduler-locking off用于关闭锁定当前线程。

我们以一个小的示例来说明这三个选项的使用吧。编写如下代码:

<code> 

101

202

303

404

505

long

g

=

0

;

606

707

void*

worker_thread_1(void*

p)

808

{

909

while

(true)

1010

{

1111

g

=

100

;

1212

printf("worker_thread_1\n");

1313

usleep(300000);

1414

}

1515

1616

return

NULL

;

1717

}

1818

1919

void*

worker_thread_2(void*

p)

2020

{

2121

while

(true)

2222

{

2323

g

=

-100

;

2424

printf("worker_thread_2\n");

2525

usleep(500000);

2626

}

2727

2828

return

NULL

;

2929

}

3030

3131

int

main()

3232

{

3333

pthread_t

thread_id_1;

3434

pthread_create(&thread_id_1,

NULL

,

worker_thread_1,

NULL

);

3535

pthread_t

thread_id_2;

3636

pthread_create(&thread_id_2,

NULL

,

worker_thread_2,

NULL

);

3737

3838

while

(true)

3939

{

4040

g

=

-1

;

4142

printf("g=%d\n",

g);

4242

g

=

-2

;

4343

printf("g=%d\n",

g);

4444

g

=

-3

;

4545

printf("g=%d\n",

g);

4646

g

=

-4

;

4747

printf("g=%d\n",

g);

4848

4949

usleep(1000000);

5050

}

5151

5252

return

0

;

5353

}

/<code>

上述代码在主线程(main 函数所在的线程)中创建了了两个工作线程,主线程接下来的逻辑是在一个循环里面依次将全局变量 g 修改成 -1、-2、-3、-4,然后休眠 1 秒;工作线程 worker_thread_1、worker_thread_2 在分别在自己的循环里面将全局变量 g 修改成 100 和 -100。

我们编译程序后将程序使用 gdb 跑起来,三个线程同时运行,交错输出:

<code>   

xx]# g++ -g -o main main.cpp -lpthread

xx]# gdb main

3...省略部分无关输出...

4Reading

symbols from main...

r

6Starting

program: /root/xx/main

debugging using libthread_db enabled]

8...省略部分无关输出...

Thread 0x7ffff6f56700 (LWP 402)]

10worker_thread_1

Thread 0x7ffff6755700 (LWP 403)]

12g

=

-1

13g

=

-2

14g

=

-3

15g

=

-4

16worker_thread_2

17worker_thread_1

18worker_thread_2

19worker_thread_1

20worker_thread_1

21g

=

-1

22g

=

-2

23g

=

-3

24g

=

-4

25worker_thread_2

26worker_thread_1

27worker_thread_1

28worker_thread_2

29worker_thread_1

30g

=

-1

31g

=

-2

32g

=

-3

33g

=

-4

34worker_thread_2

35worker_thread_1

36worker_thread_1

37worker_thread_2

/<code>

我们按 Ctrl + C 将程序中断下来,如果当前线程不在主线程,可以先使用 info threads 和 thread id 切换到主线程:

<code> 

1

^C

2Thread

1

"main"

received

signal

SIGINT,

Interrupt.

30x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

4

(gdb)

info

threads

5

Id

Target

Id

Frame

6

*

1

Thread

0x7ffff7feb740

(LWP

1191

)

"main"

0x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

7

2

Thread

0x7ffff6f56700

(LWP

1195

)

"main"

0x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

8

3

Thread

0x7ffff6755700

(LWP

1196

)

"main"

0x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

9

(gdb)

thread

1

10

[Switching

to

thread

1

(Thread

0x7ffff7feb740

(LWP

1191

))]

11

12

(gdb)

/<code>

然后在代码 11 行和 41 行各加一个断点。我们反复执行 until 48 命令,发现工作线程 1 和 2 还是有机会被执行的。

<code> 

1

(gdb)

b

main.cpp:41

2Breakpoint 1 at 0x401205:

file

main.cpp,

line

41

.

3

(gdb)

b

main.cpp:11

4Breakpoint 2 at 0x40116e:

file

main.cpp,

line

11

.

5

(gdb)

until

48

60x00007ffff704c884

in

usleep

()

from

/usr/lib64/libc.so.6

7

(gdb)

8worker_thread_2

9

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

10

11Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

1211

g

=

100

;

13

(gdb)

14worker_thread_2

15

[Switching

to

Thread

0x7ffff7feb740

(LWP

1191

)]

16

17Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

1841

printf("g=%d\n",

g);

19

(gdb)

20worker_thread_1

21worker_thread_2

22g=-1

23g=-2

24g=-3

25g=-4

26main

()

at

main.cpp:49

2749

usleep(1000000);

28

(gdb)

29worker_thread_2

30

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

31

32Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

3311

g

=

100

;

34

(gdb)

/<code>

现在我们再次将线程切换到主线程(如果 gdb 中断后当前线程不是主线程的话),执行 set scheduler-locking on 命令,然后继续反复执行 until 48 命令。

<code> 

1

(gdb)

set

scheduler-locking

on

2

(gdb)

until

48

3

4Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

541

printf("g=%d\n",

g);

6

(gdb)

until

48

7g=-1

8g=-2

9g=-3

10g=-4

11main

()

at

main.cpp:49

1249

usleep(1000000);

13

(gdb)

until

48

14

15Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

1641

printf("g=%d\n",

g);

17

(gdb)

18g=-1

19g=-2

20g=-3

21g=-4

22main

()

at

main.cpp:49

2349

usleep(1000000);

24

(gdb)

until

48

25

26Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

2741

printf("g=%d\n",

g);

28

(gdb)

29g=-1

30g=-2

31g=-3

32g=-4

33main

()

at

main.cpp:49

3449

usleep(1000000);

35

(gdb)

until

48

36

37Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

3841

printf("g=%d\n",

g);

39

(gdb)

/<code>

我们再次使用 until 命令时,gdb 锁定了主线程,其他两个工作线程再也不会被执行了,因此两个工作线程无任何输出。

我们再使用 set scheduler-locking step 模式再来锁定一下主线程,然后再次反复执行 until 48 命令。

<code> 

1

(gdb)

set

scheduler-locking

step

2

(gdb)

until

48

3worker_thread_2

4worker_thread_1

5g=-100

6g=-2

7g=-3

8g=-4

9main

()

at

main.cpp:49

1049

usleep(1000000);

11

(gdb)

until

48

12worker_thread_2

13

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

14

15Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

1611

g

=

100

;

17

(gdb)

until

48

18worker_thread_2

19worker_thread_1

20

21Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

2211

g

=

100

;

23

(gdb)

until

48

24worker_thread_2

25

[Switching

to

Thread

0x7ffff7feb740

(LWP

1191

)]

26

27Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

2841

printf("g=%d\n",

g);

29

(gdb)

until

48

30worker_thread_1

31worker_thread_2

32g=-100

33g=-2

34g=-3

35g=-4

36main

()

at

main.cpp:49

3749

usleep(1000000);

38

(gdb)

until

48

39worker_thread_2

40

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

41

42Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

4311

g

=

100

;

44

(gdb)

until

48

45worker_thread_2

46worker_thread_1

47

48Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

4911

g

=

100

;

50

(gdb)

/<code>

可以看到使用 step 模式锁定的主线程,在使用 until 命令时另外两个工作线程仍然有执行的机会。我们再次切换到主线程,然后使用 next 命令单步调试下试试。

<code> 

1

(gdb)

info

threads

2

Id

Target

Id

Frame

3

1

Thread

0x7ffff7feb740

(LWP

1191

)

"main"

0x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

4

*

2

Thread

0x7ffff6f56700

(LWP

1195

)

"main"

worker_thread_1

(p=0x0)

at

main.cpp:11

5

3

Thread

0x7ffff6755700

(LWP

1196

)

"main"

0x00007ffff701bfad

in

nanosleep

()

from

/usr/lib64/libc.so.6

6

(gdb)

thread

1

7

[Switching

to

thread

1

(Thread

0x7ffff7feb740

(LWP

1191

))]

8

9

(gdb)

set

scheduler-locking

step

10

(gdb)

next

11Single

stepping

until

exit

from

function

nanosleep,

12which

has

no

line

number

information.

130x00007ffff704c884

in

usleep

()

from

/usr/lib64/libc.so.6

14

(gdb)

next

15Single

stepping

until

exit

from

function

usleep,

16which

has

no

line

number

information.

17main

()

at

main.cpp:40

1840

g

=

-1

;

19

(gdb)

next

20

21Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

2241

printf("g=%d\n",

g);

23

(gdb)

next

24g=-1

2542

g

=

-2

;

26

(gdb)

next

2743

printf("g=%d\n",

g);

28

(gdb)

next

29g=-2

3044

g

=

-3

;

31

(gdb)

next

3245

printf("g=%d\n",

g);

33

(gdb)

next

34g=-3

3546

g

=

-4

;

36

(gdb)

next

3747

printf("g=%d\n",

g);

38

(gdb)

next

39g=-4

4049

usleep(1000000);

41

(gdb)

next

4240

g

=

-1

;

43

(gdb)

next

44

45Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

4641

printf("g=%d\n",

g);

47

(gdb)

next

48g=-1

4942

g

=

-2

;

50

(gdb)

next

5143

printf("g=%d\n",

g);

52

(gdb)

next

53g=-2

5444

g

=

-3

;

55

(gdb)

next

5645

printf("g=%d\n",

g);

57

(gdb)

next

58g=-3

5946

g

=

-4

;

60

(gdb)

next

6147

printf("g=%d\n",

g);

62

(gdb)

next

63g=-4

6449

usleep(1000000);

65

(gdb)

next

6640

g

=

-1

;

67

(gdb)

next

68

69Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

7041

printf("g=%d\n",

g);

71

(gdb)

/<code>

此时我们发现设置了以 step 模式锁定主线程,工作线程不会在单步调试主线程时被执行,即使在工作线程设置了断点。

最后我们使用 set scheduler-locking off 取消对主线程的锁定,然后继续使用 next 命令单步调试。

<code> 

1

(gdb)

set

scheduler-locking

off

2

(gdb)

next

3worker_thread_2

4worker_thread_1

5g=-100

642

g

=

-2

;

7

(gdb)

next

8worker_thread_2

9

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

10

11Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

1211

g

=

100

;

13

(gdb)

next

14g=100

15g=-3

16g=-4

17worker_thread_2

1812

printf("worker_thread_1\n");

19

(gdb)

next

20worker_thread_1

2113

usleep(300000);

22

(gdb)

next

23worker_thread_2

24

[Switching

to

Thread

0x7ffff7feb740

(LWP

1191

)]

25

26Thread

1

"main"

hit

Breakpoint

1

,

main

()

at

main.cpp:41

2741

printf("g=%d\n",

g);

28

(gdb)

next

29

[Switching

to

Thread

0x7ffff6f56700

(LWP

1195

)]

30

31Thread

2

"main"

hit

Breakpoint

2

,

worker_thread_1

(p=0x0)

at

main.cpp:11

3211

g

=

100

;

33

(gdb)

next

34g=-1

35g=-2

36g=-3

37g=-4

38worker_thread_2

3912

printf("worker_thread_1\n");

40

(gdb)

/<code>

取消了锁定之后,单步调试时三个线程都有机会被执行,线程 1 的断点也会被正常触发。

至此,我们搞清楚了如何利用 set scheduler-locking 选项来方便我们调试多线程程序。

总而言之,熟练掌握 gdb 调试等于拥有了学习优秀 C/C++ 开源项目源码的钥匙,只要可以利用 gdb 调试,再复杂的项目,在不断调试和分析过程中总会有搞明白的一天。


分享到:


相關文章: