[Flash/Flex] 使用gdb查找多线程bug

  多线程是一个有用的工具,但是使用多线程的程序会很容易产生各种类型的在单线程程序中不会发生的bug。当多个线程同时访问相同的数据时就会存在竞态条件。同步基元,如互斥体和环境变量,在避免竞态条件是很有用的,但是当使用不当时会导致其它问题。尤其是当多个线程争用同一个互斥体,以这种方式哪个线程都无法进行下去时,死锁的情况就发生了。这篇文章演示的就是如何在FlasCC程序中使用gdb查找死锁及类似的线程bug。

死锁是什么样的

    用两个线程就可能产生一个简单的死锁,线程1和线程2,还有两个互斥体,A和B。假设线程1获取互斥体A而线程2获取互斥体B。然后线程1试图获取互斥体B,因此会等待线程2将互斥体B释放。如果线程2试图获取互斥体A,它会等待线程1的释放,然后就会导致两个线程永远的等待。

    为了发现和调试一个死锁,你可以使用调试器中断等待的线程并检查它们的状态,如它们的堆栈跟踪。例如一旦你得到这个信息,你就可以确定实际上线程已经处于死锁状态而并不是仅仅执行一些长时间运行的操作。

背景

    在开始使用gdb之前,让我们讨论一个底层Flash调试器是如何工作以及它是如何与线程相关联的。当你编译ActionScript代码进行调试时,编译器将被称为debuglines的特殊操作码插入到它所产生的ActionScript字节码中。Debuglines有两个目的:他们为编译器提供行编码信息并且它们能够在字节码中标调试器能够暂停执行你的程序的位置。当Flash播放器遇到一个debugline时,它会检查看是否需要暂停这个程序,因为其进入断点,已经完成进一步操作,或者其它任何原因。一个线程的执行只有当它遇到debugline时才会停止。〔1〕

    互斥体也可以将一个线程的执行停止:如果一个线程尝试去获取一个当前被一个不同的线程所持有的互斥体,它就会一直等待直到这个互斥体可用为止。如果几个线程同时尝试获取同一个互斥体,只有一个线程会成功并且继续执行;其它的就会等待。假设此时调试器试图中断这些线程。获取到互斥体并且正在执行的那个线程将最终遇到一个debugline,暂停运行,并将控制权交给调试器。但是那些等待获取互斥体的线程并不会将控制权交给调试器,因为在接收到调试器的停止请求后他们还没有发现debugline。因此,那些等待着互斥体的线程直到获取到互斥体,恢复运行,并且遇到debugline才会被调试器所中断。

一些FlasCC本质

    上面所讨论的适用于使用flash.concurrent.Mutex同步类的ActionScript程序。大多数FlasCC程序会使用一个互斥体的pthread版本 pthread_mutex_t 来代替。毫不奇怪,pthread_mutex_t是使用互斥体来实施的,但是pthread_mutex_t和ActionScript的互斥体类实例之间却没有一对一的映射。相反,pthread中所有的同步是由一个单独的ActionScript互斥体来处理的。当几个线程处于等待中时,因为它们调用了如pthread_mutex_lock或pthread_cond_wait函数,他们每一个短时间地持有这个互斥体。这样当一个线程持有了互斥体,调试器能够将其中断,但是这样做就阻止了其它线程获取互斥体。由于处于等待中的线程只有当它持有了互斥体才能被调试器所中断,所以在任何一个时间点只有一个等待的线程可以被调试器所控制。

一个简单的示例

    考虑到这些限制,让我们调试一个简单的死锁程序。下面就是代码:
    
  1. #include <pthread.h>
  2. #include <stdio.h>

  3. static pthread_mutex_t first_lock = PTHREAD_MUTEX_INITIALIZER;
  4. static pthread_mutex_t second_lock = PTHREAD_MUTEX_INITIALIZER;
  5. static pthread_cond_t ready_to_lock = PTHREAD_COND_INITIALIZER;
  6. static pthread_mutex_t cond_mutex = PTHREAD_MUTEX_INITIALIZER;

  7. void *second_thread(void *thrdata)
  8. {   
  9.     pthread_mutex_lock(&cond_mutex);
  10.     pthread_mutex_lock(&second_lock);
  11.     pthread_cond_signal(&ready_to_lock);    
  12.     pthread_mutex_unlock(&cond_mutex);
  13.     fprintf(stderr, "about to deadlock\n");
  14.     pthread_mutex_lock(&first_lock);
  15.     pthread_exit(NULL);
  16. }   

  17. void *first_thread(void *thrdata)
  18. {
  19.     pthread_t thread2;
  20.     pthread_mutex_lock(&first_lock);
  21.     pthread_mutex_lock(&cond_mutex);
  22.     int err = pthread_create(&thread2, NULL, second_thread, NULL);
  23.     if (err) {
  24.         perror("pthread_create");
  25.     } else {
  26.         pthread_cond_wait(&ready_to_lock, &cond_mutex);
  27.         pthread_mutex_lock(&second_lock);
  28.     }
  29.     pthread_exit(NULL);
  30. }

  31. int
  32. main(int argc, char **argv)
  33. {
  34.     pthread_t thread1;
  35.     int err = pthread_create(&thread1, NULL, first_thread, NULL);
  36.     if (err) {
  37.         perror("pthread_create");
  38.     }
  39.     pthread_exit(NULL);
  40. }
复制代码

    注意这个示例中使用了一个条件来确保两个线程确实互相形成死锁。这是为了演示示例,并不是非常实现的。通常死锁情况只是时有发生。

    开始通过编译这个程序进行调试:

    gcc -o dlock.swf -g -O0 -pthread -emit-swf dlock.c

    在调试器中运行该程序,不必麻烦地设置任何断点或任何类似的事,我们只是让程序运行至死锁然后我们将其中断看看程序卡在哪里了。由于我们单独调试每个线程,所以首先要确保启用non-stop模式:
    
  1. (gdb) set pagination off
  2. (gdb) set target-async on
  3. (gdb) set non-stop on
  4. (gdb) run
  5. Starting program: dlock.swf
复制代码

    一旦“about to deadlock”这段文字出现,在gdb中按下CTRL+C来将程序中断。你应该会看到如下的一些信息:

  1. ^C
  2. Program received signal SIGTRAP, Trace/breakpoint trap.
  3. 0x00000000 in ?? ()
  4. (gdb)
复制代码

    此时,调试器已经中断了UI线程,但是我们的死锁线程仍然试图在后台运行。但是既然他们处于死锁状态,那么它们就不能做什么。我们用info threads命令让gdb给我们展示一下每个线程在做什么:

  1. (gdb) info threads
  2. [New Worker 4]
  3. [New Worker 5]
  4.   Id   Target Id         Frame 
  5.   3    Worker 5          (running)
  6.   2    Worker 4          (running)
  7. * 1    Worker 1          0x00000000 in ?? ()
复制代码


    我们还想中断每个运行的线程来看一下它在做什么。interrupt -a命令就是做这个的:
    
  1. (gdb) interrupt -a
  2. (gdb) 
  3. [Worker 4] #2 stopped.
  4. 0x00000000 in ?? ()
复制代码

    我们的两个线程只有一个已经停止并且将控制权交给了调试器。正如上面所讨论的,这是因为一次只有一个等待的线程能够被调试器所中断。碰巧2号线程首先停止,但是当你测试这个示例时你会发现3号线程会先停止。这没关系,因为我们最终能够检查每一个线程。让我们切换到停止的那个线程看一下它正在执行什么代码:
    
  1. (gdb) thread 2
  2. [Switching to thread 2 (Worker 4)]
  3. #0  0x00000000 in ?? ()
  4. (gdb) bt
  5. #0  0x00000000 in ?? ()
  6. #1  0xf0001c0c in avm2_msleep () from remote:2.elf
  7. #2  0xf0001fa3 in kmsleep () from remote:2.elf
  8. #3  0xf001507b in __do_lock_umutex () from remote:49.elf
  9. #4  0xf001575f in _do_lock_umutex () from remote:49.elf
  10. #5  0xf0014d8b in ___umtx_op_wait_umutex () from remote:49.elf
  11. #6  0xf0014423 in k_umtx_op () from remote:49.elf
  12. #7  0xf0000ae0 in _umtx_op () from remote:2.elf
  13. #8  0xf0010e69 in __thr_umutex_lock () from remote:27.elf
  14. #9  0xf000efc3 in _mutex_lock_common () from remote:17.elf
  15. #10 0xf000f0fa in __pthread_mutex_lock () from remote:17.elf
  16. #11 0xf0011a60 in pthread_mutex_lock_exp () from remote:33.elf
  17. #12 0xf00000e3 in first_thread (thrdata=0x0) at dlock.c:31
  18. #13 0xf000d3c5 in _thread_start () from remote:7.elf
  19. #14 0xf00015ce in _thread_run () from remote:2.elf
  20. #15 0x00000000 in ?? ()
复制代码



    向后追踪的结果显示这个线程已经调用了__pthread_mutex_lock,因此我们知道了它正处于试图锁定一个互斥体的过程中。由于这个线程当前处在avm2_msleep,我们可以说它正在等待另一个线程释放这个互斥体。第12帧显示了我们从程序的第31行运行到这里:

    pthread_mutex_lock(&second_lock);

    为了看3号线程的堆栈跟踪,我们需要让其获取同步互斥体。一旦我们这样做了,它就会运行直到遇到一个debugline并将控制权交给调试器。虽然通常线程不会持有一个同步互斥体很长时间,但是当前互斥体被2号线程所持有,而这个线程在调试器中被暂停了。恢复2号线程会允许其释放互斥体并且给了3号线程一个机会,让它把控制权交给调试器:
    
  1. (gdb) c&
  2. Continuing.
  3. (gdb) 
  4. [Worker 5] #3 stopped.
  5. 0x00000000 in ?? ()
复制代码

    现在3号线程停止了,我们可以检查它的堆栈跟踪:
   
    
  1. (gdb) thread 3
  2. [Switching to thread 3 (Worker 5)]
  3. #0  0x00000000 in ?? ()
  4. (gdb) bt
  5. #0  0x00000000 in ?? ()
  6. #1  0xf0001c0c in avm2_msleep () from remote:2.elf
  7. #2  0xf0001fa3 in kmsleep () from remote:2.elf
  8. #3  0xf001507b in __do_lock_umutex () from remote:49.elf
  9. #4  0xf001575f in _do_lock_umutex () from remote:49.elf
  10. #5  0xf0014d8b in ___umtx_op_wait_umutex () from remote:49.elf
  11. #6  0xf0014423 in k_umtx_op () from remote:49.elf
  12. #7  0xf0000ae0 in _umtx_op () from remote:2.elf
  13. #8  0xf0010e69 in __thr_umutex_lock () from remote:27.elf
  14. #9  0xf000efc3 in _mutex_lock_common () from remote:17.elf
  15. #10 0xf000f0fa in __pthread_mutex_lock () from remote:17.elf
  16. #11 0xf0011a60 in pthread_mutex_lock_exp () from remote:33.elf
  17. #12 0xf0000080 in second_thread (thrdata=0x0) at dlock.c:17
  18. #13 0xf000d3c5 in _thread_start () from remote:7.elf
  19. #14 0xf00015ce in _thread_run () from remote:2.elf
  20. #15 0x00000000 in ?? ()
复制代码


    正如预期的一样,这个线程也在等待互斥体,此时我们的测试程序运行至第17行:

    pthread_mutex_lock(&first_lock);

    在我们的测试程序中,很容易看出死锁的根本原因是错误的锁定顺序。如果两个线程以相同的顺序获取锁定,死锁情况将不可能发性。可是在更复杂的程序中仅仅通过检查代码并不是很容易发现这些各种各样的问题。在这种情况下,gdb的线程调试功能将会非常有用的。


〔1〕当程序抛出一个异常时调试器也会将程序中止,但是这种情况与本文所讨论的内容是不相关的。




原文链接:http://blogs.adobe.com/flascc/2012/11/30/finding-multi-threading-bugs-with-gdb/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值