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

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

  • 使用thead safe系统函数

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

  您将看到有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排查还是很费劲的。

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

  在一些架构上它可能可以,但不应该这样使用。

    正确的解决方案恰恰是是在访问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

时间: 2024-10-29 19:11:08

不明显的多线程编程的具体Bugs的相关文章

Linux多线程编程的条件变量

在stackoverflow上看到一关于多线程条件变量的问题,题主问道:什么时候会用到条件变量,mutex还不够吗?有个叫slowjelj的人做了很好的回答,我再看这个哥们其他话题的一些回答,感觉水平好高的.这里把他的回答粘贴一下方便以后查看,原帖在这里:When is a conditional variable needed, isn't a mutex enough? Even though you can use them in the way you describe, mutexes

多线程编程核心技术总结(读周志明书籍的总结)

多线程编程核心技术总结 1.Java多线程基本技能 1.1进程和线程的概念: 进程是独立的程序,线程是在进程中独立运行的子任务. 1.2使用多线程 1.2.1实现方法:继承Thread类,重写Runnable接口. 1.2.2线程安全问题:并发修改公共的实例变量,i++,i-- 1.3线程Thread类的一些方法: currentThread() 放回代码段正在被那个线程调用 isAlive() 判断线程是否处于活动状态 sleep() 使得当前线程退出CPU片段,等待获取锁 1.4停止线程 1

Java基础知识—多线程编程(五)

概述 Java 给多线程编程提供了内置的支持.一个多线程程序包含两个或多个能并发运行的部分.程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径.使用多线程也是为了充分的利用服务器资源,提高工作效率. 线程生命周期 线程是一个动态执行的过程,它也有一个从产生到死亡的过程. 新建状态: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态.它保持这个状态直到程序 start() 这个线程. 就绪状态: 当线程对象调用了start()方法之后,该

第73课 Qt中的多线程编程

1. QThread类 (1)QThread是一个跨平台的多线程解决方案 (2)QThread以简洁易用的方式实现多线程编程 2. QThread中的关键成员函数 (1)virtual void run() :线程函数,用于定义线程功能(执行流). (2)void start():启动函数,将线程入口地址设置为run函数.启动线程,新线程开始执行run函数. (3)int exec():进入事件循环,直至调用exit().返回线程退出事件循环的返回码. (4)void terminate():强

多线程编程(进程和线程)

多线程编程(进程和线程) 1.进程:指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以启动多个线程. 2.线程:指程序中一个执行流程,一个进程中可以运行多个线程. 一.创建线程(两种方式) 二.线程的5种状态( New,Runnable,Running,Block,Dead ): 三.线程的优先级 四.守护线程 /精灵线程/后台线程 五.方法 六.同步代码锁(synchronized) 一.创建线程(两种方式): 方式1:采用继承Thread的方法 第一,继承 Thre

多线程编程基础知识

多线程编程基础知识 http://www.cnblogs.com/cy163/archive/2006/11/02/547428.html 当前流行的Windows操作系统能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线程,线程提供了多任务处理的能力.用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义.现在的大型应用软件无一不是多线程多任务处理,单线程的软件是不可想象的.因此掌握

iOS多线程编程

1. 进程,线程, 任务 进程:一个程序在运行时,系统会为其分配一个进程,用以管理他的一些资源. 线程:进程内所包含的一个或多个执行单元称为线程,线程一般情况下不持有资源,但可以使用其所在进程的资源. 任务:进程或线程中要做的事情. 在引入线程的操作系统中,通常把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位. 线程比进程更小,对其调度的开销小,能够提高系统内多个任务的并发执行程度. 一个程序至少有一个进程,一个进程至少有一个线程.一个程序就是一个进程,而一个程序中的多个任

多线程编程1-NSThread

前言 每个iOS应用程序都有个专门用来更新显示UI界面.处理用户触摸事件的主线程,因此不能将其他太耗时的操作放在主线程中执行,不然会造成主线程堵塞(出现卡机现象),带来极坏的用户体验.一般的解决方案就是将那些耗时的操作放到另外一个线程中去执行,多线程编程是防止主线程堵塞,增加运行效率的最佳方法. iOS中有3种常见的多线程编程方法: 1.NSThread 这种方法需要管理线程的生命周期.同步.加锁问题,会导致一定的性能开销 2.NSOperation和NSOperationQueue 是基于OC

Android多线程编程(一)——多线程基础

什么是进程 一个进程是一个独立(self contained)的运行环境,它可以看作一个程序或者一个应用. 什么是线程 而线程是进程中执行的一个任务,Java运行环境是一个包含了不同累和程序的单一进程.线程可以被称为轻量级进程.线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源. Android线程 Android的线程,实际上和Java的多线程编程并没有什么本质上的不同.当我们需要执行一些耗时操作,比如说发起一条网络请求时,考虑到网速等其他原因,服务器未必会立刻响应我们的请求,如