不明显的多线程编程的具体Bugs

  咱们都知道,在编写多线程程序时,咱们应该记住不少细节,好比锁,使用线程安全库等。这里有一个不太明显的bug的列表,特定于多线程程序。其中许多都没有在初学者的文档或教程中提到,但我认为每一个使用线程的人最终都会中枪。html

  • 使用thead safe系统函数

    并不是全部的系统函数或者库函数都能被安全地使用。最明显的例子之一是strtok(3),它执行字符串符号化。它在每次调用中返回下一个token,并使用全局状态来保持源字符串中的当前位置。当您阅读此函数的手册页时,linux

  您将看到有thread-safe版本:strtok_r(3)带有附加参数:使用状态变量的指针,而不是全局变量的指针。有这种功能的其余例子还有:数据库

  1. mbstowcs(3) 用 mbsrtowcs(3) 替代
  2. localtime(3) 用 localtime_r(3)替代
  3. gethostbyname(3) 用 gethostbyname_r(3) 或更好的 getaddrinfo(3)替代
  4. rand(3) 用 random_r(3)替代
  • 使用不受互斥锁保护的变量,volatile关键字误解

    你可能认为你只是在使用一个共享的“简单”变量,好比它是一个没有mutex的布尔变量。安全

1 bool stop = false;
2  
3 while (!stop) {
4         sleep (1);
5 }

  上述代码在开启编译优化的状况下是不可能被其余线程经过设置stop变量为true来中断的。这是由于编译器能够自由应用优化:一种缘由是当编译器发现该变量在循环中没有被修改时,它能够省略while条件。另外一种缘由是,根据系统的架构,网络

  这种内存上的变化可能没有被其余处理器注意到。第一种状况,当时在调试一个数据库应用时有遇到过,当时状况是:在一个过程当中,初始化一个局部变量后,balabala进行了一大堆操做,而后才使用该变量。最后测试发现结果不对,在我多多线程

  次调试后才发现该变量一直处于未初始化状态的默认值。这还一度让我认为该不会是给该变量的赋值操做没起做用形成的,最后没招,我尝试提早了该变量的使用位置,结果就行了。。。这时我才忽然意识到极可能是编译优化的问题形成的。架构

  这种因为编译优化形成的bug排查仍是很费劲的。dom

    volatile关键字有时被视为是一种解决方案,但它与线程无关。此关键字旨在用于底层代码(如设备驱动程序),只是为了确保写入设备的内存等。在多线程进程中它并不能作到咱们须要的:它不能使内存中的内容的变化被其余处理器可见。socket

  在一些架构上它可能能够,但不该该这样使用。函数

    正确的解决方案偏偏是在访问stop变量时使用mutex,即便它是如此“简单”的内存访问。

  • 二次关闭以及对无效文件描述符的使用 

    考虑以下代码片断:  

 1 fd = open ("file", O_RDONLY);
 2 if (fd < 0) exit (1);
 3  
 4 while ((res = read (fd, buf, sizeof(buf)))) {
 5     if (res < 0) {
 6         close(fd);
 7         fprintf (stderr, "Read error!\n");
 8         break;
 9     }
10     else {
11         printf ("Read %zd bytes\n", res);
12     }
13 }
14  
15 close(fd);

  哪有问题?在单线程程序中,它能正常工做,即便有bug存在:在第4行发生读取错误的状况下,文件描述符将被关闭两次 - 第15行的close(2)将只返回一个将被忽略的错误。然而在多线程程序中使用这段代码会让你陷入麻烦,

  一般很讨厌。为何?由于第15行的第二次close(3)可能不会失败。这里存在race condition:若是其余线程在第一次与第二次close(3)之间打开了一个file或者建立了一个socket而且得到了相同的fd,那么上述线程会关闭它。

  要知道,文件描述符在同一进程的线程之间是共享的。关闭其余线程的fd可能不是最糟糕的可能发生的状况,试想:若是上述代码的第二个close()以前尝试进行了写操做,这将致使会向其余线程的文件或者TCP链接进行写操做!

  二次关闭是多线程中可能发生的最难发现的bug之一。由于这种race condition不多复现而且结果一般是很奇怪的错误。做为一种解决方法:建议常常检查每个close(3)的返回值。可是一般在程序中不会去检查,特别是当fd只是用于

  读文件的状况,固然,这要首先看读文件会不会失败了。若是用日志记录每次close(3)失败的状况,咱们就能够在race condition发生以前发现这种bug。在大多数状况下,第二个close(3)更有可能失败而不是会去关闭其余线程的fd。

  • 未捕获异常

    未捕获的异常将致使进程退出并显示错误消息。当编写多进程网络daemon程序时,这样的错误将终止一个进程,而且正确编写的程序将从新产生该错误。当这样的守护进程被转换为多线程设计时,未捕获的异常更危险:

  由于它将kill整个程序,而不仅是一个线程。因此必须记住这一点而且在最顶层代码的某处捕获一切异常,即便是经过下面这种方式:

1 try
2    ... 
3 catch(...)
4 { log(“unknown exception”) }

  catch(...)而不是从新抛出异常是虽然是一个不太好的作法,但至少程序仍然能够处理其他的客户端请求。这多是惟一catch(...)的状况。

  • 使用fork()系统调用

    关于多线程的进程与fork()的东西,后面的文章我会进行总结,也能够先看open(2)以及dup3(2)的O_CLOEXEC标记的使用说明。但基本上:在多线程进程中没有安全的方式使用fork(),

  而且在子进程中作不止是执行execve()的事情。由于你不能知道fork()调用时其余线程在作什么,一些mutex可能已经被一些线程持有了,一些线程可能正在修改一些复杂数据的过程当中等等。

  • mutex处于锁定状态下执行IO操做

    这里是一个性能提示:避免在持有互斥量的同时进行I/O操做。至少要避免I/O操做,最好是在mutex被锁定的状况下避免任何系统调用或甚至库调用。

  相信我:你不会但愿在一个很是繁忙的网络daemon进程中每秒至少处理数千个请求的线程等待一些恰巧在持有mutex的状况下经过syslog(3)系统调用写一些错误消息的线程。使用互斥体只是为了同步对内存的访问,

  并尽快解锁它们。看下面这个例子:

1 pthread_mutex_lock (&mutex); 2 if (freeSlots == 0) { 3     syslog (LOG_ERR, "No slots available, rejecting request"); 4 } else { 5     freeSlots--; 6 } 7 pthread_mutex_unlock (&mutex);

  在syslog(3)调用时,mutex已经处于被持有状态。根据syslog守护程序的配置和机器的负载,当在每一个日志行以后执行fsync()时,这甚至可能须要几十或几百毫秒来完成。因此在进行日志记录以前,只需解锁互斥,

  这样其余线程就能够运行而不须要等待I/O完成。

  • 建议:包装一个Mutex类

    若是你使用的是C ++语言,不要直接使用POSIX mutexes函数。建立一个Mutex类会容易不少,这样就能够在构造函数中得到锁,而在析构函数中释放锁。这种方式只是建立该类的自动变量,但它会在构造函数中得到锁,

  并在代码的做用域结束时因析构函数而自动解锁。这种类的一个示例是Boost库中的scoped_lock

相关文章
相关标签/搜索