Chinaunix首页 | 论坛 | 博客
  • 博客访问: 63392
  • 博文数量: 7
  • 博客积分: 90
  • 博客等级: 民兵
  • 技术积分: 117
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-26 09:09
文章分类

全部博文(7)

文章存档

2018年(1)

2017年(4)

2012年(2)

我的朋友

分类: C/C++

2017-09-25 18:03:20


     最近在敲一些线程相关的代码, 线程里有自己分配一些资源, 分配是简单, 但是到了要释放分配的资源那一步 就郁闷了,
     要考虑各种异常终止的情况, 代码乱糟糟不说, 容易出问题, 有可能还没释放完全,造成内存泄漏.

     下面是一步步的历程.

1. 先给出线程里最原始的需求代码  不加其他异常处理.


点击(此处)折叠或打开

  1. static int call_sth (char *str)
  2. {
  3.     char *call_res = (char *)calloc(1, 128);

  4.     printf ("call_sth: calloc str: %p\n", call_res);

  5.     // 从外部程序中获取一些数据进行处理
  6.     // ...


  7.     free(call_res);

  8.     return 0;
  9. }

  10. static void *my_func1(void *arg)
  11. {
  12.     char *str = (char *)arg;
  13.     char *ou = (char *)calloc(1, 128);
  14.     int ret = 0;

  15.     printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);

  16.     ret = call_sth(str);

  17.     snprintf(ou, 128, "this is my_func1's result: %d\n", ret);

  18.     pthread_exit((void *)ou);
  19. }

  20. typedef void* thread_fun(void *);
  21. static pthread_t pthread_start(thread_fun func, void *arg)
  22. {
  23.     pthread_t tid;
  24.     int ret = 0;

  25.     pthread_attr_t attr;
  26.     pthread_attr_init(&attr);
  27.     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);


  28.     memset(&tid, 0, sizeof(tid));

  29.     ret = pthread_create(&tid, &attr, func, arg);
  30.     if (ret != 0) {
  31.         memset(&tid, 0, sizeof(tid));
  32.     }

  33.     return tid;
  34. }

  35. int my_test(void)
  36. {
  37.     int ret;
  38.     pthread_t tid;
  39.     char *res_str = NULL;

  40.     tid = pthread_start(my_func1, "Hello pthread 1 ");
  41.     if (!pthread_t_is_zero(tid)) {
  42.         while (1) { //每1秒看一次线程有没有退出
  43.           if (!pthread_is_alive(tid)) {
  44.             //线程退出 获取返回值
  45.             pthread_join(tid, (void **)&res_str);
  46.             if (res) {
  47.                 if (res == PTHREAD_CANCELED) {
  48.                     printf("ID为0x%x的线程被CANCEL终止\n", (unsigned int)tid);
  49.                 }
  50.                 else {
  51.                     printf("获取ID为0x%x的线程结果: %s\n", (unsigned int)tid, res_str);
  52.                     free(res_str);
  53.                     res_str = NULL;
  54.                 }
  55.             }
  56.             memset(&tid, 0, sizeof(tid));
  57.           }
  58.           else
  59.             sleep(1);
  60.         }
  61.     }

  62.     return 0;
  63. }

    从代码可以看出, 很简单的一个线程, 线程里 分配一次空间存储动态数据 并返回.

    但是 考虑到如果出现用户退出或其他情况需要中止线程的情况呢
    例如:
        call_str()函数 正在从外部获取数据的时候 收到pthread_cancel()发出的停止命令,
         这里, 如果在free()前面就有cancel point, 那么后面的free()不会执行 线程就被结束了, 更不用说父函数my_func1最开始分配的ou 也没被释放的问题了.


1. 使用pthread_cleanup_push() pthread_cleanup_pop()



点击(此处)折叠或打开

  1. static void * my_free(void *arg)
  2. {
  3.     printf(" free %p\n", arg);

  4.     free(arg);
  5. }

  6. static int call_sth (char *str)
  7. {
  8.     char *call_res = (char *)calloc(1, 128);
  9.     char *call_res1 = (char *)calloc(1, 256);

  10.     printf("call res: %p call_res1: %p\n", call_res, call_res1);
  11.     pthread_cleanup_push(my_free, call_res);
  12.     pthread_cleanup_push(my_free, call_res1);

  13.     printf ("call_sth: calloc str: %p\n", call_res);

  14.     // get the data to call_res
  15.     // ...
  16.     sleep(5); //为了测试这里睡眠几秒让主线程有时间pthread_cancel()

  17.     pthread_cleanup_pop(call_res);
  18.     pthread_cleanup_pop(call_res1);

  19.     return 0;
  20. }

  21. static void *my_func1(void *arg)
  22. {
  23.     char *str = (char *)arg;
  24.     char *ou = (char *)calloc(1, 128);
  25.     int ret = 0;

  26.     printf("ou: %p\n", ou);
  27.     pthread_cleanup_push(my_free, (void *)ou);

  28.     printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);

  29.     ret = call_sth(str);

  30.     snprintf(ou, 128, "this is my_func1's result: %d\n", ret);

  31.     pthread_cleanup_pop(0);

  32.     pthread_exit((void *)ou);
  33. }
    我只粘贴了部分代码, 只是想展示使用pthread_cleanup_push() 和pthread_cleanup_pop() 来防止异常终止 内存泄漏的情况.
    可以看到, push() pop()必须成对出现,  而且每有一个动态分配的内存 如果其使用过程中有cancel point 导致其可能内存泄漏 就得使用push() pop()这样处理.
缺点 :
    1. 如果分配的地方多, push() pop()将到处都是. 如果你想一个函数来释放所有分配的空间...
            嗷, 你又得发时间来弄个分配空间管理程序, 一个函数释放所有分配的空间, 如果你执意如此, 祝你顺利, 可以略过后面内容, 敲代码去吧!
    2. 另一方面  通过man看函数说明, 可以看到push() pop() 是以宏的方式来展开的, 所以push() pop()要成对出现.
           如果 你在一个循环里面多次分配/重新分配呢 , 直接会导致编译失败 例如下面这样:

点击(此处)折叠或打开

  1. static int call_sth (char *str)
  2. {
  3.     char *call_res = (char *)calloc(1, 128);
  4.     char *call_res1 = (char *)calloc(1, 256);
  5.     char *fuck_p = NULL;
  6.     int len = 0;

  7.     printf("call res: %p call_res1: %p\n", call_res, call_res1);
  8.     pthread_cleanup_push(my_free, call_res);
  9.     pthread_cleanup_push(my_free, call_res1);

  10.     printf ("call_sth: calloc str: %p\n", call_res);

  11.     for (;;) {

  12.         // get the data and len
  13.         // ...
  14.         len = 256;

  15.         if (!fuck_p) {
  16.             fuck_p = (char *) calloc(1, len);
  17.             printf ("call_sth: calloc fuck_p: %p\n", fuck_p);

  18.             pthread_cleanup_push(my_free, fuck_p); //编译失败 错误
  19.         }

  20.         sleep(1);
  21.     }
  22.     pthread_cleanup_pop(fuck_p);

  23.     pthread_cleanup_pop(call_res);
  24.     pthread_cleanup_pop(call_res1);

  25.     return 0;
  26. }

2. 使用pthread_key_create()  pthread_getspecific() pthread_setspecific()

   这几个函数是用来保存线程私有数据的, 但是pthread_key_create(pthread_key_t *key, void (*destructor)(void*)) 可以为这个key映射的值指定释放函数.
    当线程退出时如果这个key的值不为空 那么会自动调用这个函数. 借助这个我们也可以做一些释放分配内存的操作.


点击(此处)折叠或打开

  1. static void my_free(void *arg)
  2. {
  3.     printf(" free %p\n", arg);

  4.     free(arg);
  5. }
  6. static pthread_key_t key[10];
  7. static pthread_once_t key_once = PTHREAD_ONCE_INIT;

  8. static void make_key()
  9. {
  10.     int i = 0;

  11.     for (i=0; i<10; i++) {
  12.         pthread_key_create(&key[i], my_free);
  13.     }
  14. }

  15. static int call_sth (char *str)
  16. {
  17.     char *call_res = (char *)calloc(1, 128);
  18.     char *call_res1 = (char *)calloc(1, 256);
  19.     char *fuck_p = NULL;
  20.     int len = 0;

  21.     printf("call res: %p call_res1: %p\n", call_res, call_res1);

  22.     pthread_setspecific(key[1], call_res);
  23.     pthread_setspecific(key[2], call_res1);

  24.     for (;;) {

  25.         // get the data and len
  26.         // ...
  27.         len = 256;

  28.         if (!fuck_p) {
  29.             fuck_p = (char *) calloc(1, len);
  30.             printf ("call_sth: calloc fuck_p: %p\n", fuck_p);

  31.             pthread_setspecific(key[3], fuck_p);
  32.         }

  33.         sleep(1);
  34.     }

  35.     return 0;
  36. }

  37. static void *my_func1(void *arg)
  38. {
  39.     char *str = (char *)arg;
  40.     char *ou = (char *)calloc(1, 128);
  41.     int ret = 0;

  42.     printf("ou = %p\n", ou);

  43.     /* 保证只调用make_key初始化一次 */
  44.     pthread_once(&key_once, make_key);

  45.     pthread_setspecific(key[0], ou);

  46.     printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);

  47.     ret = call_sth(str);

  48.     snprintf(ou, 128, "this is my_func1's result: %d\n", ret);

  49.     /* 因为要返回这个指针指向的内容,不能释放,
  50.      * 所以得赋为空 防止退出线程时被自动释放了. */
  51.     pthread_setspecific(key[0], NULL);

  52.     pthread_exit((void *)ou);
  53. }
 
    我只粘贴了部分代码, 只是想展示使用pthread_key_create() pthread_setspecific() 来防止异常终止 内存泄漏的情况.

    可以看到, 这类函数不像上面说的push() pop()那样以宏的方式展开 还需要成队出现, 比较省事. 但缺点同push() pop()类似

缺点 :
    1. 事先得知道大概要分配多少次内存 和 内存的数据类型(要用什么释放函数), 根据这些先初始化好key,  如果是其他类型的数据需要其他的函数来释放, 得单独另外的初始化.
     例如:
       我得到了cJSON *json = cJSON_Parse(buff), 数据,  要为这个json 使用key的话, key必须要初始化释放函数为 cJSON_Delete()才行, 用free肯定是不得行的.
       pthread_key_t json_key;
       pthread_key_create(json_key, cJSON_Delete);
       pthread_key_setspecific(json_key, json);
   2. 容易弄混, 如果要为每一个要用的内存指针 分配一个单独的名字key 太麻烦.
       如果直接为同一类型的内存指针 分配一个数组 像上面代码中所述那样,  使用起来容易搞混,  可能这个子函数里用了key[2] 那个子函数里用了key[3], 这样下去会剪不断 理还乱, 崩溃...

3. 不是办法的办法

      究其根源, 为什么会有异常中止的情况, 其实大部分情况下 都是正常的流程 直到线程正常终止,  异常中止只是我个人的YY.

       大型程序中是可以弄一个内存管理程序的, 线程退出时统一释放, 这里不讨论. 

      小/中型程序中 如果像上面那样弄得太复杂 极有可能漏掉某个分配的没有处理 导致内存泄漏; 如果业务允许 不用立即退出, 可以让其执行完业务后自动退出, 正常释放, 代码将会简单很多.
      例如: 当主线程需要退出的时候, 直接分离正在运行的子线程, 让其走完正常的流程自己释放自己分配空间 执行完后 系统将回收子线程资源, 整个世界都安静了~
        注:  如果 要返回一些内容给父线程, 可以退出前先查看父线程是否存在, 不存在的话, 自己释放用来返回给父线程的内存空间 再退出.

省事,简单,有效的方法:
      1. 使用 "全局变量" "套接字传数据" 等其他方法 来控制子线程的退出, 而不是直接粗暴的pthread_cancel()
      2. 设置子线程 PTHREAD_CANCEL_DISABLE , 不允许被其他线程异常中止.
      3. 子线程 自己按照正常逻辑分配 - 使用 - 释放 内存空间 直到线程终止.

    完整的代码如下:

点击(此处)折叠或打开

  1. /*pthread_kill的返回值:成功(0) 线程不存在(ESRCH) 信号不合法(EINVAL)*/
  2. enum {
  3.     PTHREAD_RET_ALIVE = 1,
  4.     PTHREAD_RET_ESRCH,
  5.     PTHREAD_RET_EINVAL,
  6. };

  7. static int pthread_test(pthread_t tid)
  8. {
  9.     int pthread_kill_err;
  10.     pthread_kill_err = pthread_kill(tid,0);

  11.     if(pthread_kill_err == ESRCH) {
  12.         printf("ID为0x%x的线程不存在或者已经退出。\n",(unsigned int)tid);
  13.         return PTHREAD_RET_ESRCH;
  14.     }
  15.     else if(pthread_kill_err == EINVAL) {
  16.         printf("发送信号非法。/n");
  17.         return PTHREAD_RET_EINVAL;
  18.     }

  19.     printf("ID为0x%x的线程目前仍然存活。\n",(unsigned int)tid);
  20.     return PTHREAD_RET_ALIVE;
  21. }
  22. #define pthread_is_alive(tid) (pthread_test(tid) != PTHREAD_RET_ESRCH)

  23. static inline int pthread_t_is_zero(pthread_t tid)
  24. {
  25.     pthread_t zero_tid;
  26.     memset(&zero_tid, 0, sizeof(zero_tid));

  27.     return !memcmp(&tid, &zero_tid, sizeof(pthread_t));
  28. }

  29. static int call_sth (char *str)
  30. {
  31.     char *call_res = (char *)calloc(1, 128);
  32.     char *call_res1 = (char *)calloc(1, 256);
  33.     char *fuck_p = NULL;
  34.     int len = 0;

  35.     for (;;) {

  36.         // get the data and len
  37.         // ...
  38.         len = 256;

  39.         if (!fuck_p) {
  40.             fuck_p = (char *) calloc(1, len);
  41.         }

  42.         sleep(1);
  43.     }

  44.     if (call_res)
  45.         free(call_res);
  46.     if (call_res1)
  47.         free(call_res1);
  48.     if (fuck_p)
  49.         free(fuck_p);

  50.     return 0;
  51. }

  52. typedef struct pthread_struct_t{
  53.     char * data;
  54.     pthread_t p_tid;
  55. }p_t;

  56. static void *my_func1(void *arg)
  57. {
  58.     p_t * p = (p_t *)arg;
  59.     char *str = p->data;
  60.     char *ou = (char *)calloc(1, 128);
  61.     int ret = 0;

  62.     /* 不允许pthread_cancel() 终止 */
  63.     pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);

  64.     printf("ou = %p\n", ou);

  65.     printf("线程1(ID:0x%x)退出。str(%s) \n",(unsigned int)pthread_self(), str);

  66.     ret = call_sth(str);

  67.     snprintf(ou, 128, "this is my_func1's result: %d\n", ret);

  68.     /* 父线程不在了, 不用返回数据了 释放分配的内存空间 */
  69.     if (!pthread_is_alive(p->p_tid) && ou) {
  70.         free(ou);
  71.         ou = NULL;
  72.     }

  73.     pthread_exit((void *)ou);
  74. }

  75. typedef void* thread_fun(void *);
  76. static pthread_t pthread_start(thread_fun func, void *arg)
  77. {
  78.     pthread_t tid;
  79.     int ret = 0;

  80.     pthread_attr_t attr;
  81.     pthread_attr_init(&attr);
  82.     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);


  83.     memset(&tid, 0, sizeof(tid));

  84.     ret = pthread_create(&tid, &attr, func, arg);
  85.     if (ret != 0) {
  86.         memset(&tid, 0, sizeof(tid));
  87.     }

  88.     return tid;
  89. }

  90. int my_test(void)
  91. {
  92.     int ret;
  93.     pthread_t tid;
  94.     char *res_str = NULL;
  95.     int count = 0 ;
  96.     p_t p;

  97.     p.p_tid = pthread_self();
  98.     p.data = "Hello pthread 1";
  99.     tid = pthread_start(my_func1, &p);
  100.     if (!pthread_t_is_zero(tid)) {
  101.         while (1) {
  102.             /* 1s过后想停止掉子线程 */
  103.             if (count > 0) {
  104. #if USE_CANCEL
  105.                 printf("cancel pthread[0x%x]\n", (unsigned int)tid);
  106.                 pthread_cancel(tid);
  107.                 sleep(1);
  108. #else
  109.                 printf("detach pthread[0x%x]\n", (unsigned int)tid);
  110.                 pthread_detach(tid);
  111.                 break;
  112. #endif
  113.             }

  114.             if (!pthread_is_alive(tid)) {
  115.                 pthread_join(tid, (void **)&res_str);
  116.                 if (res_str) {
  117.                     if (res_str == PTHREAD_CANCELED) {
  118.                         printf("ID为0x%x的线程被CANCEL终止\n", (unsigned int)tid);
  119.                     }
  120.                     else {
  121.                         printf("获取ID为0x%x的线程结果: %s\n", (unsigned int)tid, res_str);
  122.                         free(res_str);
  123.                         res_str = NULL;
  124.                     }
  125.                 }
  126.                 memset(&tid, 0, sizeof(tid));
  127.                 break;
  128.             }
  129.             else {
  130.                 sleep(1);
  131.                 count ++;
  132.             }
  133.         }
  134.     }

  135.     return 0;
  136. }

   再看看这边的代码  一下子清静了...

阅读(2241) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~