使用pthread cancel终止线程的填坑历程

首先给大家推荐一下我老师大神的人工智能教学网站。教学不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵黄段子!点这里可以跳转到网站

开头说明一句:使用pthread_cancel是一个丧心病狂的想法。

首先是常识

pthread_cancel(thread)会发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
若是在整个程序退出时,要终止各个线程,应该在成功发送 CANCEL 指令后,使用 pthread_join 函数,等待指定的线程已经完全退出以后,再继续执行;否则,很容易产生 “段错误”。

然后是进一步的认识

设置本线程对Cancel信号的反应
int pthread_setcancelstate(int state, int *oldstate);
state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE
分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。

设置本线程取消动作的执行时机
int pthread_setcanceltype(int type, int *oldtype);
type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS
仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。   

手动创建一个取消点
void pthread_testcancel(void)
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。 此函数在线程内执行,执行的位置就是线程退出的位置,在执行此函数以前,线程内部的相关资源申请一定要释放掉,他很容易造成内存泄露。

那么,问题来了…

/*
这是一段伪代码,看懂意思就成
main里面不断判断线程,如果test_thd存在就删除,如果不存在,就创建.
这里面创建线程的属性统一为缺省状态(PTHREAD_CANCEL_ENABLE).
*/

static sem_t mutex;
static int iswork = 0;  //用来判断线程状态
static void *test_thread_handler();
static void *select_read();

int main( int argc, char *argv[] )
{
    pthread_t test_thd;
    iswork = 0;
    sem_init(&mutex, 0, 1 ); //初始化锁
    while(1)
    {
        get_task();  //阻塞等待任务到来(消息队列阻塞)
        if(iswork)
        {
            pthread_cancel(test_thd);
            iswork = 0;
        }
        else
        {
            sem_wait(&mutex);  //加锁  (0,0)->(0,1)->(0,0)
            write();  //写串口
            sem_post(&mutex);  //解锁  (0,0)->(0,1)

            pthread_create(&test_thd,NULL, test_thread_handler,NULL);
        }
    }

    return 0;
}

static void *test_thread_handler()
{
    iswork = 1;
    int time = 5;   //5秒
    pthread_detach(pthread_self());     //分离
    int ret = socket_select(time);//一个5秒的阻塞,时间内有触发立即返回1
    printf("select over %d\t%d\n",ret,time);
    if(ret > 0)
    {
        select_read();
    }
    printf("thread over\n");
    iswork = 0;
    return 0;
}

static void select_read()
{
    sem_wait(&mutex);  //加锁  (0,0)->(0,1)->(0,0)
    read();  //读串口,未确定是否为阻塞(取消点) 
    sem_post(&mutex);  //解锁  (0,0)->(0,1)
    printf("read over\n");
}

可以看出来,pthread_cancel(test_thd)时会有4种情况:
1. test_thd已经运行结束,取消失败

select over 0 0
read over
thread over

2.test_thd正阻塞在socket_select,假设已经阻塞了2秒

select over 0 3
thread over
这里至今搞不太明白第二种情况为什么明明已经cancel了线程,但程序还是会执行下去,打印 thread over

3.test_thd正阻塞在read,假设socket_select在1秒内就收到了信号

select over 1 4
一直阻塞

4.test_thd正运行在sem_wait(&mutex);之前
不打印

很明显,第三种情况就使线程陷入了死锁,但还没搞懂程序是阻塞在了main里面的sem_wait(),还是阻塞在了test里面的sem_post()

填坑历程,学到不少


先入为主认为消息队列阻塞

很不幸在发现bug的时候对线程的cancel仅仅只有普通的常识和简单的认识。
同时由于项目的read时间的确是极短,使得bug很难重现,于是第一时间认为阻塞的根源在于get消息队列的时候。

static void PrintSysInfo(struct msginfo *stInfo)
{
    printf("-------------- MSG_INFO --------------\n");
    printf("msg queue num       : %d\n",stInfo->msgpool);
    printf("msg total num       : %d\n",stInfo->msgmap);
    printf("msg total len       : %d\n",stInfo->msgtql);

    printf("single msg max len       : %d\n",stInfo->msgmax);
    printf("all msg max len       : %d\n",stInfo->msgmnb);
    printf("Max msg num       : %d\n",stInfo->msgmni);
    printf("--------------------------------------\n");

    return;
}

    msgctl(qid_ipc, MSG_INFO, &stMsq);
    PrintSysInfo((struct msginfo*)&stMsq);//打印系统当前队列信息
    msgctl(qid_ipc, IPC_STAT, &stMsq);
    printf("%u",(int)stMsq.msg_qnum);//打印当前队列消息条数

也是巧了,发现设备的linux内核里面的消息队列最大空间仅有16k,而我每条消息有的甚至超过2k 来个八九条就成撑满,一般来说也不会同时阻塞了八九条消息这么多,然后我深以为此乃问题所在。遂修改数据结构,把每条消息大小缩减至150字节。虽然最后没有解决根源问题,但也解决了这么一个隐藏bug。

老大提示可能是编译器优化导致

优化了消息队列的数据结构之后,发现问题还是没有得到解决,此时还没想到是程序陷入了死锁的原因,单纯认为只是阻塞了,于是把点放到iswork这个变量上。
路过的老大给了我一个C语言的关键字:volatile
原本就孤陋寡闻的我瞬间茅厕顿开,差点就高潮了,有一种认为自己还是图样图森破的觉悟,后来发现自己果然还是图样图森破。
附volatile关键字的用法和使用情景。

volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果不加入volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。

某些编译器会对变量做一些优化,当编译器发现连续两次读一个数据的时候没有在代码里面对这个变量进行过操作,就会把这个数据的值拷到一个临时的数据中,下次再读这个数据的时候会直接返回临时的数据,而不是去读这个数据本身。

在本次线程内,当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;
当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致

当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

此致向我老大敬礼。

加入大量打印,sleep延迟调试,终于定位出问题根源

由于read的时间实在太短,bug的重现使debug遇到很大的阻碍,再推翻很多设想后,重新加入sleep一点一点得测,终于皇天不负有心人,发现程序是陷入死锁了。这本来是个很容易就能想到的问题,但是我刚想到的时候我就直接进在每次读之前手动解锁,但是依然没有效果,我就略过了这个罪魁恶首。

        if(iswork)
        {
            //如果正在读,等它读完再锁,在取消线程,再解锁
            sem_wait(&mutex);  //加锁  (0,0)->(0,1)->(0,0)
            pthread_cancel(test_thd);
            sem_post(&mutex);  //解锁  (0,0)->(0,1)
            iswork = 0;
        }
        else
        {
            sem_wait(&mutex);  //加锁  (0,0)->(0,1)->(0,0)
            write();  //写串口
            sem_post(&mutex);  //解锁  (0,0)->(0,1)

            pthread_create(&test_thd,NULL, test_thread_handler,NULL);
        }

至此,问题得到解决,历时三天。
需求功能是本地从消息队列获取任务,通过TTL串口发送任务到目标设备,目标设备会先后返回两个信号:1.收到后回复B码,2.处理后回复N码。
本机收到B码后要去消息队列等待下一个任务,同时等待目标设备N码。如果在等待N码的过程中有紧急任务要处理,会直接发送新的数据到目标设备。

本问题的根源终其原因应该是架构问题。
最理想的解决方案当然应该是起两条线程,一条专门去读,一条专门去写,长时间存在,互不干扰。

但是在做的过程中有太多其他细节和各种因素,最终只能使用在一条线程中不断创建删除线程去实现。其中更改需求的辛酸过来人都懂吧。

今晚想吃爆炒羊驼。

点这里可以跳转到人工智能网站

发表评论