线程

来自:

https://blog.youkuaiyun.com/qq_34328833/article/details/60142733

http://www.cnblogs.com/xuxm2007/archive/2011/04/01/2002162.html

https://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/

多线程

复习中掌握线程的基本管理即可,而不用考虑线程的同步:

创建线程花费的代价,比创建进程小得多,所以同一个进程的,多个线程执行多个任务——>比多个进程执行多个任务更有效率。

线程也分为用户级线程、内核级线程——对于前者,多个线程之间的上下文切换,由用户决定;对于后者,则由系统决定。(二者一般是1:1或者1:n的对应关系)

多线程程序的编译时,一定记得要加入动态库,例如:gcc k.c -o k -lpthread,但运行时却不用加。

首先#include <pthread.h>

线程的创建:int pthread_create(&线程名字,NULL,线程执行函数名,传递的参数*arg),执行函数必须是这种:void *func(),可以带参数,也可以不带,函数体随意。成功则返回0

线程的等待(所谓等待,就是函数名称的字面意思)int pthread_join(直接是线程名字,保存线程返回值的指针void **returnValue)。成功则返回0

线程的退出:void pthread_exit(void *returnValue),返回值就是一个字符串,可以由其他函数,如等待函数pthread_join来检测获取其返回值。

#include <stdio.h>

#include <pthread.h>

 

void *func(void *arg)

{

    char   *p="bye bye";

    int     i;

 

    printf("arg=%d\n", *(int *)arg);

    while(1)

    {

      scanf("%d",&i);

      printf("get %d\n", i);

      if(0==i)

        pthread_exit(p);

    }

}

 

int main()

{

    int res;

    pthread_t first_thread;

    int share_int = 10;

    res = pthread_create(&first_thread, NULL, func, (void *)&share_int);

    printf("res=%d, thread=%d\n", res,first_thread );

 

    void *returnValue;

    res = pthread_join(first_thread, &returnValue);

    printf("returnValue=%s\n", (char *)returnValue);

    return 0;

}

 

gdb

先介绍一下GDB多线程调试的基本命令。

info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程。

thread ID 切换当前调试的线程为指定ID的线程。

break thread_test.c:123 thread all 在所有线程中相应的行上设置断点

thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command

thread apply all command 让所有被调试线程执行GDB命令command

 

set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

 6.set scheduler-locking [off|on|step]

  值得注意的是,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。

  off:不锁定任何线程,也就是所有线程都执行,这是默认值。

  on:只有当前被调试程序会执行。

  step:在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

 7.show scheduler-locking,查看当前锁定线程的模式。

gdb对于多线程程序的调试有如下的支持:

 

线程产生通知:在产生新的线程时, gdb会给出提示信息

(gdb) r

Starting program: /root/thread

[New Thread 1073951360 (LWP 12900)]

[New Thread 1082342592 (LWP 12907)]---以下三个为新产生的线程

[New Thread 1090731072 (LWP 12908)]

[New Thread 1099119552 (LWP 12909)]

 

查看线程:使用info threads可以查看运行的线程。

(gdb) info threads

  4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

  3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

  2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb)

 

注意,行首的蓝色文字为gdb分配的线程号,对线程进行切换时,使用该该号码,而不是上文标出的绿色数字。

 

另外,行首的红色星号标识了当前活动的线程

 

切换线程:使用 thread THREADNUMBER 进行切换,THREADNUMBER 为上文提到的线程号。下例显示将活动线程从 切换至 4

(gdb) info threads

   4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb) thread 4

[Switching to thread 4 (Thread 1099119552 (LWP 12940))]#0   0xffffe002 in ?? ()

(gdb) info threads

* 4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

   1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb)

 

后面就是直接在你的线程函数里面设置断点,然后continue到那个断点,一般情况下多线程的时候,由于是同时运行的,最好设置 set scheduler-locking on

 

 

这样的话,只调试当前线程

介绍完gdb调试多线程的基本命令,下面就让我们以一个简单的栗子来使用并熟悉这些命令吧!

  

[cpp] view plain copy

#include<stdio.h>  

#include<pthread.h>  

  

void *pthread1_run(void *arg)  

{  

    int count=10;  

    while(count--)  

    {  

        sleep(1);  

        printf("i am new pthread1 %lu\n",pthread_self());  

    }  

    pthread_exit(NULL);  

    return 0;  

}  

  

void *pthread2_run(void *arg)  

{  

    int count=10;  

    while(count--)  

    {  

        sleep(1);  

        printf("i am new pthread2 %lu\n",pthread_self());  

    }  

    pthread_exit(NULL);  

    return 0;  

}  

  

int main()  

{  

    pthread_t tid1;  

    pthread_t tid2;  

    pthread_create(&tid1,NULL,pthread1_run,NULL);  

    pthread_create(&tid2,NULL,pthread2_run,NULL);  

    pthread_join(tid1,NULL);  

    pthread_join(tid2,NULL);  

    return 0;  

}  

  

Makefile  

  

testtid:testtid.c  

    gcc -o $@ $^ -lpthread -g   //-g是说明要使用gdb调试该代码,-lpthread是与线程有关函数编译链接的时候必须添加的  

.PHONY:clean  

clean:  

    rm -f testtid  

 

 

  1.查看当前正在调试的线程并切换线程

  

      前面的123gdb分配的线程号,当切换线程的时候使用该线程号。最前面的'*'号说明当前正在调试的线程。

 

  2.让所有被调试的线程都执行同一个命令,就是打印堆栈信息

  

 

3.只让线程编号为1的线程打印堆栈信息

 

 

 4.锁定线程并查看当前锁定的线程

 

 

Linux 上线程开发 API 的概要介绍

多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。

线程,互斥锁,条件在 Linux 平台上对应的 API 可以用表 1 归纳。为了方便熟悉 Windows 线程编程的读者熟悉 Linux 多线程开发的 API,我们在表中同时也列出 Windows SDK 库中所对应的 API 名称。

表 1. 线程函数列表
对象操作Linux Pthread APIWindows SDK 库对应 API
线程创建pthread_createCreateThread
退出pthread_exitThreadExit
等待pthread_joinWaitForSingleObject
互斥锁创建pthread_mutex_initCreateMutex
销毁pthread_mutex_destroyCloseHandle
加锁pthread_mutex_lockWaitForSingleObject
解锁pthread_mutex_unlockReleaseMutex
条件创建pthread_cond_initCreateEvent
销毁pthread_cond_destroyCloseHandle
触发pthread_cond_signalSetEvent
广播pthread_cond_broadcastSetEvent / ResetEvent
等待pthread_cond_wait / pthread_cond_timedwaitSingleObjectAndWait

多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。

 

Linux 线程编程中的 5 条经验

尽量设置 recursive 属性以初始化 Linux 的互斥变量

互斥锁是多线程编程中基本的概念,在开发中被广泛使用。其调用次序层次清晰简单:建锁,加锁,解锁,销毁锁。但是需要注意的是,与诸如 Windows 平台的互斥变量不同,在默认情况下,Linux 下的同一线程无法对同一互斥锁进行递归加速,否则将发生死锁。

所谓递归加锁,就是在同一线程中试图对互斥锁进行两次或两次以上的行为。其场景在 Linux 平台上的代码可由清单 1 所示。

清单 1. Linux 重复对互斥锁加锁实例
// 通过默认条件建锁
     pthread_mutex_t *theMutex = new pthread_mutex_t;
     pthread_mutexattr_t attr;
     pthread_mutexattr_init(&attr);
     pthread_mutex_init(theMutex,&attr);
     pthread_mutexattr_destroy(&attr);
 
     // 递归加锁
     pthread_mutex_lock (theMutex);
     pthread_mutex_lock (theMutex);
     pthread_mutex_unlock (theMutex);
     pthread_mutex_unlock (theMutex);

在以上代码场景中,问题将出现在第二次加锁操作。由于在默认情况下,Linux 不允许同一线程递归加锁,因此在第二次加锁操作时线程将出现死锁。

Linux 互斥变量这种奇怪的行为或许对于特定的某些场景会所有用处,但是对于大多数情况下看起来更像是程序的一个 bug 。毕竟,在同一线程中对同一互斥锁进行递归加锁在尤其是二次开发中经常会需要。

这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起 recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。请看清单 2 。

清单 2. 设置互斥锁 recursive 属性实例
pthread_mutexattr_init(&attr);
     // 设置 recursive 属性
     pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);
     pthread_mutex_init(theMutex,&attr);

因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。

注意 Linux 平台上触发条件变量的自动复位问题

条件变量的置位和复位有两种常用模型:第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。其状态变化如图 1 所示:

图 1. Windows 的条件变量状态变化流程
Windows 的条件变量状态变化流程

第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。其状态变化如图 2 所示:

图 2. Linux 的条件变量状态变化流程
Linux 的条件变量状态变化流程

具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。而对于 Windows,当调用 SetEvent 触发 Auto-reset 的 Event 条件时,如果没有被条件阻塞的线程,那么条件将维持在触发状态,直到有新的线程被条件阻塞并被释放为止。

这种差异性对于那些熟悉 Windows 平台上的条件变量状态模型而要开发 Linux 平台上多线程的程序员来说可能会造成意想不到的尴尬结果。试想要实现一个旅客坐出租车的程序:旅客在路边等出租车,调用条件等待。出租车来了,将触发条件,旅客停止等待并上车。一个出租车只能搭载一波乘客,于是我们使用单一触发的条件变量。这个实现逻辑在第一个模型下即使出租车先到,也不会有什么问题,其过程如图 3 所示:

图 3. 采用 Windows 条件变量模型的出租车实例流程
索引使用的容量要求

然而如果按照这个思路来在 Linux 上来实现,代码看起来可能是清单 3 这样。

清单 3. Linux 出租车案例代码实例
……
  // 提示出租车到达的条件变量
  pthread_cond_t taxiCond;
 
  // 同步锁
  pthread_mutex_t taxiMutex;
 
  // 旅客到达等待出租车
  void * traveler_arrive(void * name) {
     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
     pthread_mutex_lock(&taxiMutex);
     pthread_cond_wait (&taxiCond, &taxtMutex);
     pthread_mutex_unlock (&taxtMutex);
     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
     pthread_exit( (void *)0 );
  }
 
  // 出租车到达
  void * taxi_arrive(void *name) {
     cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
     pthread_cond_signal(&taxtCond);
     pthread_exit( (void *)0 );
  }
 
  void main() { 
     // 初始化
     taxtCond= PTHREAD_COND_INITIALIZER;
     taxtMutex= PTHREAD_MUTEX_INITIALIZER;
     pthread_t thread;
     pthread_attr_t threadAttr;
     pthread_attr_init(&threadAttr);
 
     pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));
     sleep(1);
     pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));
     sleep(1);
     pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));
     sleep(1);
 
     return 0;
  }

好的,运行一下,看看结果如清单 4 。

清单 4. 程序结果输出
Taxi Jack arrives.
     Traveler Susan needs a taxi now!
     Taxi Mike arrives.
     Traveler Susan now got a taxi.

其过程如图 4 所示:

图 4. 采用 Linux 条件变量模型的出租车实例流程
图 4. 采用Linux条件变量模型的出租车实例流程

通过对比结果,你会发现同样的逻辑,在 Linux 平台上运行的结果却完全是两样。对于在 Windows 平台上的模型一, Jack 开着出租车到了站台,触发条件变量。如果没顾客,条件变量将维持触发状态,也就是说 Jack 停下车在那里等着。直到 Susan 小姐来了站台,执行等待条件来找出租车。 Susan 搭上 Jack 的出租车离开,同时条件变量被自动复位。

但是到了 Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。

条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后 Linux 函数如清单 5 所示。

清单 5. Linux 出租车案例代码实例
……
  // 提示出租车到达的条件变量
  pthread_cond_t taxiCond;
 
  // 同步锁
  pthread_mutex_t taxiMutex;
 
  // 旅客人数,初始为 0
  int travelerCount=0;
 
  // 旅客到达等待出租车
  void * traveler_arrive(void * name) {
     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” << endl ;
     pthread_mutex_lock(&taxiMutex);
 
     // 提示旅客人数增加
     travelerCount++;
     pthread_cond_wait (&taxiCond, &taxiMutex);
     pthread_mutex_unlock (&taxiMutex);
     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
     pthread_exit( (void *)0 );
  }
 
  // 出租车到达
  void * taxi_arrive(void *name)
  {
     cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
 
  while(true)
  {
         pthread_mutex_lock(&taxiMutex);
 
         // 当发现已经有旅客在等待时,才触发条件变量
         if(travelerCount>0)
         {
             pthread_cond_signal(&taxtCond);
             pthread_mutex_unlock (&taxiMutex);
             break;
         }
         pthread_mutex_unlock (&taxiMutex);
     }
 
     pthread_exit( (void *)0 );
  }

因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。

注意条件返回时互斥锁的解锁问题

在 Linux 调用 pthread_cond_wait 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。

Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单 6 所示:

清单 6. 条件变量返回后的解锁实例
void * traveler_arrive(void * name) {
     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
     pthread_mutex_lock(&taxiMutex);
     pthread_cond_wait (&taxiCond, &taxtMutex);
     pthread_mutex_unlock (&taxtMutex);
     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
     pthread_exit( (void *)0 );
  }

这一点对于熟悉 Windows 平台多线程开发的开发者来说尤为重要。 Windows 上的 SignalObjectAndWait() 函数是常与 Linux 平台上的 pthread_cond_wait() 函数被看作是跨平台编程时的一对等价函数。但是需要注意的是,两个函数退出时的状态是不一样的。在 Windows 平台上,SignalObjectAndWait(HANDLE a, HANDLE b, …… ) 方法在调用结束返回时的状态是 a 和 b 都是置位(signaled)状态,在普遍的使用方法中,a 经常是一个 Mutex 变量,在这种情况下,当返回时,Mutex a 处于解锁状态(signaled),Event b 处于置位状态(signaled), 因此,对于 Mutex a 而言,我们不需要考虑解锁的问题。而且,在 SignalObjectAndWait() 之后,如果需要对临界区数据进行重新访问,都需要调用 WaitForSingleObject() 重新加锁。这一点刚好与 Linux 下的 pthread_cond_wait() 完全相反。

Linux 对于 Windows 的这一点额外解锁的操作区别很重要,一定得牢记。否则从 Windows 移植到 Linux 上的条件等待操作一旦忘了结束后的解锁操作,程序将肯定会发生死锁。

等待的绝对时间问题

超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。但是如果你是第一次使用这个 API 时,首先你需要了解的就是这个 API 当中超时参数的特殊性(就如本节标题所提示的那样)。我们首先来看一下这个 API 的定义。 pthread_cond_timedwait() 定义请看清单 7 。

清单 7. pthread_cond_timedwait() 函数定义
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex,
              const struct timespec *restrict abstime);
参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。这对于拥有 Windows 平台线程开发经验的人来说可能尤为困惑。因为 Windows 平台下所有的 API 等待参数(如 SignalObjectAndWait,等)都是相对时间,
假设我们指定相对的超时时间参数如 dwMilliseconds (单位毫秒)来调用和超时相关的函数,这样就需要将 dwMilliseconds 转化为 Linux 下的绝对时间参数 abstime 使用。常用的转换方法如清单 8 所示:
清单 8. 相对时间到绝对时间转换实例
/* get the current time */
     struct timeval now;
     gettimeofday(&now, NULL);
     
     /* add the offset to get timeout value */
     abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;
     abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。

正确处理 Linux 平台下的线程结束问题

在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义如清单 9 。

清单 9. pthread_join 函数定
int pthread_join(pthread_t th, void **thread_return);

调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。

如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。其申明如清单 10 。

清单 10. pthread_detach 函数定义
int pthread_detach(pthread_t th);

另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如清单 11 。

清单 11. 创建 detach 线程代码实例
pthread_t       tid;
     pthread_attr_t  attr;
     pthread_attr_init(&attr);
     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
     pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。

总结与补充

本文以上部分详细介绍了 Linux 的多线程编程的 5 条高效开发经验。另外你也可以考虑尝试其他一些开源类库来进行线程开发。

1. Boost 库

Boost 库来自于由 C++ 标准委员会类库工作组成员发起,致力于为 C++ 开发新的类库的 Boost 组织。虽然该库本身并不是针对多线程而产生,但是发展至今,其已提供了比较全面的多线程编程的 API 支持。 Boost 库对于多线程支持的 API 风格上更类似于 Linux 的 Pthread 库,差别在于其将线程,互斥锁,条件等线程开发概念都封装成了 C++ 类,以方便开发调用。 Boost 库目前对跨平台支持的很不错,不仅支持 Windows 和 Linux ,还支持各种商用的 Unix 版本。如果开发者想使用高稳定性的统一线程编程接口减轻跨平台开发的难度, Boost 库将是首选。

待整理:

查看线程的命令:

查看各个线程内存,CPU情况

Top -H -p 进程ID

   PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                    
184217 root      20   0  409m 6516 4628 S  0.0  0.0   0:00.03 vs
184242 root      20   0  409m 6516 4628 S  0.0  0.0   0:36.18 vs
184243 root      20   0  409m 6516 4628 S  0.0  0.0   2:53.08 vs

查看线程的分叉情况:

pstree -p 进程ID

[root@localhost kthread]# pstree -p 184217
VS(184217)-+-{VS}(184242)
                  |-{vs}(184243)
                  |-{vs}(184244)
                  |-{vs}(184245)
                  `-{vs}(184246)

查看进程运行协议栈,可以看到各个线程停止在哪里

pstack 进程ID

Thread 6 (Thread 0x7f6941964700 (LWP 184242)):
#0  0x0000003ff4ade523 in select () from /lib64/libc.so.6
#1  0x00007f69444b04ed in _eXosip_read_message () from /usr/local/mds/ms_cgsl/lib/libeXosip2.so.11
#2  0x00007f694449c0ac in eXosip_execute () from /usr/local/mds/ms_cgsl/lib/libeXosip2.so.11
#3  0x00007f694449d077 in _eXosip_thread () from /usr/local/mds/ms_cgsl/lib/libeXosip2.so.11
#4  0x0000003ff56077f1 in start_thread () from /lib64/libpthread.so.0
#5  0x0000003ff4ae593d in clone () from /lib64/libc.so.6
Thread 5 (Thread 0x7f6940f63700 (LWP 184243)):
#0  0x0000003ff4ade523 in select () from /lib64/libc.so.6
#1  0x00007f69444b6c4b in eXosip_event_wait () from /usr/local/mds/ms_cgsl/lib/libeXosip2.so.11
#2  0x0000000000412a18 in CeXSip::startListen() ()
#3  0x00000000004081c0 in serverListenThread(void*) ()
#4  0x0000003ff56077f1 in start_thread () from /lib64/libpthread.so.0
#5  0x0000003ff4ae593d in clone () from /lib64/libc.so.6
Thread 4 (Thread 0x7f6940562700 (LWP 184244)):
#0  0x0000003ff4aaadcd in nanosleep () from /lib64/libc.so.6
#1  0x0000003ff4aded94 in usleep () from /lib64/libc.so.6
#2  0x0000000000408250 in timeOutCheckThread(void*) ()
#3  0x0000003ff56077f1 in start_thread () from /lib64/libpthread.so.0
#4  0x0000003ff4ae593d in clone () from /lib64/libc.so.6
Thread 3 (Thread 0x7f693fb61700 (LWP 184245)):
#0  0x0000003ff4aaadcd in nanosleep () from /lib64/libc.so.6
#1  0x0000003ff4aded94 in usleep () from /lib64/libc.so.6
#2  0x00000000004082e2 in spaceCheckProcThread(void*) ()
#3  0x0000003ff56077f1 in start_thread () from /lib64/libpthread.so.0
#4  0x0000003ff4ae593d in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f693f160700 (LWP 184246)):
#0  0x0000003ff4aaadcd in nanosleep () from /lib64/libc.so.6
#1  0x0000003ff4aded94 in usleep () from /lib64/libc.so.6
#2  0x000000000041a60a in CDog::sendFood() ()
#3  0x000000000041a5de in CDog::sendFoodThread(void*) ()
#4  0x0000003ff56077f1 in start_thread () from /lib64/libpthread.so.0
#5  0x0000003ff4ae593d in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f69419667e0 (LWP 184217)):
#0  0x0000003ff4aaadcd in nanosleep () from /lib64/libc.so.6
#1  0x0000003ff4aaac40 in sleep () from /lib64/libc.so.6
#2  0x000000000040e840 in main ()

统计每步系统调用花费的时长

strace -o output.txt -T -tt -e trace=all -p 进程ID

 

 

众所周知linux中命令catmoreless均可用来查看文件内容,主要区别有:
cat是一次性显示整个文件的内容,还可以将多个文件连接起来显示,它常与重定向符号配合使用,适用于文件内容少的情况;
moreless一般用于显示文件内容超过一屏的内容,并且提供翻页的功能。morecat强大,提供分页显示的功能,lessmore更强大,提供翻页,跳转,查找等命令。而且moreless都支持:用空格显示下一页,按键b显示上一页。

Gdb原理:

gdb主要功能的实现依赖于一个系统函数ptrace,通过man手册可以了解到,ptrace可以让父进程观察和控制其子进程的检查、执行,改变其寄存器和内存的内容,主要应用于打断点(也是gdb的主要功能)和打印系统调用轨迹。

 

一、ptrace函数

函数原型如下:

#include <sys/ptrace.h>

Long ptrace(enum__ptrace_request request, pid_t pid, void*addr,void*data);

ptrace系统调用的request主要选项

 

PTRACE_TRACEME

表示本进程将被其父进程跟踪,交付给这个进程的所有信号(除SIGKILL之外),都将使其停止,父进程将通过wait()获知这一情况。

PTRACE_ATTACH

attach到一个指定的进程,使其成为当前进程跟踪的子进程,子进程的行为等同于它进行了一次PTRACE_TRACEME操作。

PTRACE_CONT

继续运行之前停止的子进程。可同时向子进程交付指定的信号。

 

更多参数请man ptrace

 

二、gdb使用ptrace的基本流程

gdb调试一个新进程:通过fork函数创建一个新进程,在子进程中执行ptrace(PTRACE_TRACEME, 0, 0, 0)函数,然后通过execv()调用准备调试的程序。

attach到已运行进程:将pid传递给gdb,然后执行ptrace(PTRACE_ATTACH, pid, 0, 0)

在使用参数为PTRACE_TRACEMEPTRACE_ATTACHptrace系统调用建立调试关系之后,交付给目标程序的任何信号(除SIGKILL之外)都将被gdb先行截获,gdb因此有机会对信号进行相应处理,并根据信号的属性决定在继续目标程序运行时是否将之前截获的信号实际交付给目标程序。

 

三、gdb使用的内核机制

断点的功能是通过内核信号实现的,以x86为例,内核向某个地址打入断点,实际上就是往该地址写入断点指令INT 3,即0xCC。目标程序运行到这条指令之后就会触发SIGTRAP信号,gdb捕获到这个信号,根据目标程序当前停止位置查询gdb维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中。

 

内核是通过如下调用进入内核态的:

 

SYSCALL_DEFINE4(ptrace, long, request, long, pid, long, addr, long, data)

 

根据不同的request调用不同函数,基本都是判断当前进程taskptrace选项,走security_ptrace函数,在linux security模块中,然后汇编。待续

Gdb使用

先介绍一下GDB多线程调试的基本命令。

Bt显示当前执行到代码行。

gdb +程序名  运行(gdb main

使用 "--tui" 参数,可以在终端窗口上部显示一个源代码查看窗。很方便。

gdb --tui main

list(l) 查看最近10行源码

list fun 查看fun函数源码

list file:fun 查看file文件中的fun函数源码

list num1 num2 查看num1~num2行的源码

 

break 行号

break fun 在函数处设置断点

break file:行号 

break file:fun

break if <condition> 条件成立时程序停住

info break (i b)查看断点

watch expr expr的值发生改变时,程序停住

delete  n  删除断点

 info breakpoints显示所有断点

 

删除断点:

断点的删除与断点的设置同样的重要。删除断点的命令有两个:
delete
用法:delete [breakpoints num] [range...]
delete可删除单个断点,也可删除一个断点的集合,这个集合用连续的断点号来描述。
例如:
delete 5
delete 1-10

1.以行号设置断点

   (gdb)break 7

   (gdb)run

2.以函数名设置断点

(gdb)break function_name

(gdb)run

3.以条件表达式设置断点

(gdb)break 7 if i==99

(gdb)run

4.另一种,以表达式设置断点的方法

   (gdb)watch i==99

   这个命令必须在变量i被定义之后才会成功运行,为了解决这个问题,首先在变量 被定义的后一行设置中断,然后使用run命令运行程序,程序暂停后就可以使用watch i==99设置断点了。

##############################

单步执行

(gdb)next

#############################

查看当前设置的断点信息

(gdb)info breakpoints


###############################

使中断失效(断点仍然存在)或有效

(gdb)info breakpoints

(gdb)disable b_id           //使中断失效,b_id 为中断编号

(gdb)info breakpoints

(gdb)enable b_id         //使中断有效,b_id 为中断编号

(gdb)info breakpoints

##############################

删除断点

clear : 删除程序中所有的断点

clear 行号 删除这行的断点

clear 函数名 删除该函数的断点

delete b_id1 b_id2 ... : 删除指定编号的断点

##################################

查看和设置变量值

1.print命令

    print 变量或表达式:打印变量或表达式当前的值。

    print 变量=值:对变量进行赋值。

    print 表达式@要打印的值的个数n:打印以表达式开始的n个数

2.whatis命令:显示某个变量或表达式值的数据类型

   whatis 变量或表达式

   例子:

             

3.set命令:给变量赋值

        set variable 变量=

       【提示】set命令还可以针对远程调试进行设置,可以用来设置gdb一行的字符数等。

 


clear
用法:clear 
    删除所在行的多有断点。
    clear location
clear 删除所选定的环境中所有的断点
clear location location描述具体的断点。
例如:
clear list_insert         //删除函数的所有断点
clear list.c:list_delet   //删除文件:函数的所有断点
clear 12                  //删除行号的所有断点
clear list.c:12           //删除文件:行号的所有断点

clear 删除断点是基于行的,不是把所有的断点都删除。

 

断点激活和静止:

对断点的控制除了建立和删除外,还可以通过使能和禁止来控制,后一种方法更灵活。
断点的四种使能操作:
enable [breakpoints] [range...] 完全使能
enable                //激活所有断点
enable 4            //激活4断点
enable 5-6            //激活56断点
disable [breakpoints] [range...] 禁止
用法举例:
diable                //禁止所有断点
disble 2            //禁止第二个断点
disable 1-5            //禁止第1到第5个断点

enable once [breakpoints] [range...] 使能一次,触发后禁止
enable delete [breakpoints] [range...]使能一次,触发后删除

 

 

run/r 启动程序

continue(c) 运行至下一个断点

step(s) 单步跟踪,进入函数

next(n) 单步跟踪,不进入函数

finish 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数信息

until 运行程序直到程序退出循环体

print i (p i) 查看变量的值

ptype 查看变量类型

print array 查看数组

print *array@len 查看动态内存

print x=5 改变运行时的数据

print &array 查看数组的地址

回车代表上一个命令

 

core文件

在程序崩溃时,一般会生成一个文件叫core文件。core文件记录的是程序崩溃时的内存映像,并加入调试信息。core文件生成的过程叫做core dump

设置生成core文件

ulimit -c 查看core-dump状态

ulimit -c 数字 

ulimit -c unlimited 

gdb利用core文件调试

gdb 文件名 core文件

Backtrace(bt)   查看堆栈

 

多线程调试

info threads 显示当前可调试的所有线程

thread ID 切换当前调试的线程为指定ID的线程

attach process-id gdb状态下,开始调试一个正在运行的进程

thread apply all command 所有线程执行command

 

info threads 显示当前可调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。 前面有*的是当前调试的线程。

thread ID 切换当前调试的线程为指定ID的线程。

break thread_test.c:123 thread all 在所有线程中相应的行上设置断点

thread apply ID1 ID2 command 让一个或者多个线程执行GDB命令command

thread apply all command 让所有被调试线程执行GDB命令command

 

set scheduler-locking off|on|step 估计是实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

 6.set scheduler-locking [off|on|step]

  值得注意的是,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。

  off:不锁定任何线程,也就是所有线程都执行,这是默认值。

  on:只有当前被调试程序会执行。

  step:在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。

 7.show scheduler-locking,查看当前锁定线程的模式。

gdb对于多线程程序的调试有如下的支持:

 

线程产生通知:在产生新的线程时, gdb会给出提示信息

(gdb) r

Starting program: /root/thread

[New Thread 1073951360 (LWP 12900)]

[New Thread 1082342592 (LWP 12907)]---以下三个为新产生的线程

[New Thread 1090731072 (LWP 12908)]

[New Thread 1099119552 (LWP 12909)]

 

查看线程:使用info threads可以查看运行的线程。

(gdb) info threads

  4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

  3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

  2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb)

 

注意,行首的蓝色文字为gdb分配的线程号,对线程进行切换时,使用该该号码,而不是上文标出的绿色数字。

 

另外,行首的红色星号标识了当前活动的线程

 

切换线程:使用 thread THREADNUMBER 进行切换,THREADNUMBER 为上文提到的线程号。下例显示将活动线程从 切换至 4

(gdb) info threads

   4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb) thread 4

[Switching to thread 4 (Thread 1099119552 (LWP 12940))]#0   0xffffe002 in ?? ()

(gdb) info threads

* 4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()

   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()

   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()

   1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(gdb)

 

后面就是直接在你的线程函数里面设置断点,然后continue到那个断点,一般情况下多线程的时候,由于是同时运行的,最好设置 set scheduler-locking on

线程可以返回一个值,可以是0等,也可以是一个指针。join后,根据不同的返回值进行处理。比如可以是

 pthread_exit((void*)"the first return!"); //结束线程,返回一个值


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值