pthread的同步:死锁问题

现在可以继续在前文的读写线程上讨论。但对正要讨论的问题来说,那个例子太臃肿了。不停翻屏查代码也很累。所以重新从网上找了个例子。但是所讨论的问题,对前文的读写线程的例子都是适用的。新例子的出处在:
https://blog.csdn.net/qq_39852676/article/details/121368186

事先声明一下,这里引用它,并不代表认为它就是对的。相反,里面实际含有大把的错误。这里引用它只不过因为篇幅比较小。并且在这里顺手把它的错误都改正了。免得新手程序员把它当范例,那样就被带偏了。这个例子是:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


struct msg
{
    int num; 
    struct msg *next; 
};
 
struct msg *head = NULL;    
struct msg *temp = NULL;    


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
 
void *producer(void *arg)
{
    while (1)   
	{
        pthread_mutex_lock(&mutex);        

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;                       
        printf("---producered---%d\n", temp->num);

        pthread_mutex_unlock(&mutex);       
        pthread_cond_signal(&has_producer); 
        usleep(rand() % 3000);             
    }
 
    return NULL;
}
 
void *consumer(void *arg)
{
    while (1)       
	{
        pthread_mutex_lock(&mutex);    
        while (head == NULL)            
	    {
            pthread_cond_wait(&has_producer, &mutex);   
        }
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);                     
        temp = NULL;                    
        pthread_mutex_unlock(&mutex);   

        usleep(rand() % 3000);          
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t ptid, ctid;
    srand(time(NULL));     

    
    pthread_create(&ptid, NULL, producer, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);

    
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
 
    return 0;
}

这样写代码,在我这里当然是要打叉得0分的。mutex不明所以。pthread_cond_signal() 明明已经脱靶了,又用 pthread_cond_wait() 飞靶来接。最后usleep() ,这里明明有竞争条件没有处理,用个sleep来掩饰。总共没几行代码,被我这么批评,竟然没有一句是对的?

在我的前文已经说明。pthread_cond_wait()中用的mutex不是互斥量,而是对休眠线程的检测方法。因为线程的全文加锁,只有在休眠时解锁。测试这个锁就可以知道线程有没有休眠。跟你竞争的线程休眠了,就是单线程运行了,所有的数据随你操作,又有什么临界区不临界区的?

为了继续本文的讨论,现在需要对这个例子中的代码调整一下。对consumer线程的代码下手。首先,把循环里的pthread_mutex_lock()移到while之前,循环里的pthread_mutex_unlock()删除。这样consumer线程就是全文加锁。把pthread_cond_wait()的while循环也删了。这个循环不清不楚,没接住就没接住,出了bug就正面找原因,不乱改对接代码。当然为了小心,读者自己写代码可以在下面这样写捕捉错误,本文从略:

  pthread_cond_wait(&has_producer, &mutex);   
  if(head == NULL)  {
  		printf("unexpected wait wakeup.\n");
  		errorlog++;
  }

最后两个usleep()也都删了。“这样不行,会出问题的”。对。这正是本文的重点。
这样改过之后程序变为:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


struct msg
{
    int num;
    struct msg *next;
};

struct msg *head = NULL;
struct msg *temp = NULL;


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
int cons_sync;

void *producer(void *arg)
{
    int len;
    while (1)  {
        pthread_mutex_lock(&mutex);

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;
        len=0;
        while(temp) { len++; temp=temp->next;}
        printf("---producered(%d)---%d\n", len, head->num);

        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&has_producer);
        while(cons_sync==0){
                sched_yield();
        }
        cons_sync=0;
    }

    return NULL;
}

void *consumer(void *arg)
{
    pthread_mutex_lock(&mutex);
    cons_sync=999;
    while (1)      {
        pthread_cond_wait(&has_producer, &mutex);
        cons_sync=999;
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);
        temp = NULL;
    }

    return NULL;
}

int main(void)
{
    pthread_t ptid, ctid;


    srand(time(NULL));


    pthread_create(&ctid, NULL, consumer, NULL);
    {
        while(cons_sync==0){
                sched_yield();
        }
        cons_sync=0;
    }
    pthread_create(&ptid, NULL, producer, NULL);


    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);

    return 0;
}


producer线程增加了len计数器,这样每次打印都监视链表的长度。并在最后增加一个cons_sync同步。这里有竞争条件。无论 pthread_cond_signal()放在 pthread_mutex_unlock()的前面还是后面,下一句回到循环头上的 pthread_mutex_lock(),都会和consumer的退出wait产生锁竞争,所以这里要确保consumer的wait先退出,然后producer再回去循环头上锁。因为锁已经被consumer占了,producer就会阻塞,一直到consumer执行完进入下一次wait。

现在可以立即看一下修改效果。producer的pthread_cond_signal()每一个都被consumer接住。producer和consumer交替打印语句。producer语句中显示的链表长度始终是1。

到此,原来的例子改完了。运行也一切正常。但这和死锁有什么关系?

事实上,producer最后的那个cons_sync大家都担心会不会影响性能。虽然应该不会,还是希望把它去掉。那么分两次上锁怎么样?consumer在进入wait前,先上锁一个空锁,再休眠。producer发signal给consumer后,不是直接去竞争mutex锁,而是去锁这个空锁,这样会阻塞。consumer恢复执行,解锁这个空锁,这时consumer已经有了mutex锁,解锁空锁后,producer继续运行,就阻塞循环头上的那个mutex锁上了。经过这样一套魔幻手法,看起来一切OK,感觉就要成功了。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


struct msg
{
    int num;
    struct msg *next;
};

struct msg *head = NULL;
struct msg *temp = NULL;


pthread_mutex_t idle = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
volatile int cons_sync;

void *producer(void *arg)
{
    int len;
    while (1)  {
        pthread_mutex_lock(&mutex);

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;
        len=0;
        while(temp) { len++; temp=temp->next;}
        printf("---producered(%d)---%d\n", len, head->num);

        pthread_cond_signal(&has_producer);
        pthread_mutex_unlock(&mutex);
        pthread_mutex_lock(&idle);
        pthread_mutex_unlock(&idle);
    }

    return NULL;
}

void *consumer(void *arg)
{
    pthread_mutex_lock(&mutex);
    cons_sync=999;
    while (1)      {
        pthread_mutex_lock(&idle);
        pthread_cond_wait(&has_producer, &mutex);
        pthread_mutex_unlock(&idle);
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);
        temp = NULL;
    }

    return NULL;
}

int main(void)
{
    pthread_t ptid, ctid;


    srand(time(NULL));


    pthread_create(&ctid, NULL, consumer, NULL);
    {
        while(cons_sync==0){
                sched_yield();
        }
        cons_sync=0;
    }
    pthread_create(&ptid, NULL, producer, NULL);


    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);

    return 0;
}

这样能跑,producer打印出来的长度也是1,跟consumer的交替打印也一直正常。但是跑着跑着突然就不动了。多线程编程经常遇到这种情况。大家不会手足无措。但这是什么原因呢?

这里出现了死锁问题。OS的调度使得两个线程并不是像我们设想的那样运行。当consumer被producer的signal唤醒之后,两个线程都处在激活状态。它们执行串绕交织,在直到下一次阻塞之前,其中一个线程的任何一条语句都有可能在另一个线程的每一条语句之前执行。

所以consumer对空锁的解锁出现在producer对它的上锁之前并不奇怪,而且也是允许的。出现这个逆序不会造成问题。但是如果OS继续扣住producer在 idle的上锁之前,直到consumer的本次任务执行完。consumer会先对 idle上锁,然后去休眠。等到OS终于放行producer,producer去做idle上锁,但是这个锁已经被占了。于是producer被阻塞。consumer需要producer的signal来唤醒休眠,producer需要consumer的解锁来解除阻塞。这样就死锁了。

危险区域出现在producer向consumer发signal之后到它对idle上锁之间。过了这块区域又安全了。producer进入危险区时做个标记,出了危险区再做个标记,让consumer避开,这样就行了。实际写代码会把危险区再放大一点点到producer对idle解锁之后,这样可以防止consumer在idle锁上多做一次无意义的阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


struct msg
{
    int num;
    struct msg *next;
};

struct msg *head = NULL;
struct msg *temp = NULL;


pthread_mutex_t idle = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_producer = PTHREAD_COND_INITIALIZER;
volatile int cons_sync;

void *producer(void *arg)
{
    int len;
    cons_sync=1;
    while (1)  {
        pthread_mutex_lock(&mutex);

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;
        len=0;
        while(temp) { len++; temp=temp->next;}
        printf("---producered(%d)---%d\n", len, head->num);

        pthread_cond_signal(&has_producer);
        cons_sync=0;
        pthread_mutex_unlock(&mutex);
        pthread_mutex_lock(&idle);
        pthread_mutex_unlock(&idle);
        cons_sync=1;
    }

    return NULL;
}

void *consumer(void *arg)
{
    pthread_mutex_lock(&mutex);
    cons_sync=999;
    while (1)      {
        while(cons_sync==0){
                sched_yield();
        }
        pthread_mutex_lock(&idle);
        pthread_cond_wait(&has_producer, &mutex);
        pthread_mutex_unlock(&idle);
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);
        temp = NULL;
    }

    return NULL;
}

int main(void)
{
    pthread_t ptid, ctid;


    srand(time(NULL));


    pthread_create(&ctid, NULL, consumer, NULL);
    {
        while(cons_sync==0){
                sched_yield();
        }
        cons_sync=0;
    }
    pthread_create(&ptid, NULL, producer, NULL);


    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);

    return 0;
}

注意危险区的起始标记cons_sync=0需要放在 pthread_mutex_unlock(&mutex); 之前。如果放在之后,consumer已经唤醒,consumer可能会抢在cons_sync=0这句之前运行,检查到过期的cons_sync标记后误以为已经到了安全区。这样就失败了。

好了。改完了。运行也正常了。现在终于可以松一口气了。

可是,有没有发现有什么不对劲? 对,为了避免影响性能刚刚去掉的那个cons_sync,现在为了避免死锁有加回来了。

难道就没有办法了吗?有的,大家都用条件变量。producer发signal给consumer然后用条件变量阻塞并解锁mutex,consumer发signal给producer然后用条件变量阻塞并解锁mutex,阻塞和解锁一步完成。这样就没有竞争条件了。最后这是用条件变量的方案:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


struct msg
{
    int num;
    struct msg *next;
};

struct msg *head = NULL;
struct msg *temp = NULL;


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER;

int cons_sync;
int wc;

void *producer(void *arg)
{
    int len;

    pthread_mutex_lock(&mutex);
    while (1)  {

        temp = malloc(sizeof(struct msg));
        temp->num = rand() % 100 + 1;
        temp->next = head;
        head = temp;
        len=0;
        while(temp) { len++; temp=temp->next;}
        printf("---producered(%d)---%d\n", len, head->num);

        pthread_cond_signal(&cond_producer);
        pthread_cond_wait(&cond_consumer, &mutex);
    }

    return NULL;
}

void *consumer(void *arg)
{
    pthread_mutex_lock(&mutex);
    cons_sync=999;
    while (1)      {
        pthread_cond_wait(&cond_producer, &mutex);
        temp = head;
        head = temp->next;
        printf("------------------consumer--%d\n", temp->num);
        free(temp);
        temp = NULL;
        pthread_cond_signal(&cond_consumer);
    }

    return NULL;
}

int main(void)
{
    pthread_t ptid, ctid;


    srand(time(NULL));


    pthread_create(&ctid, NULL, consumer, NULL);
    { while(cons_sync==0) sched_yield(); cons_sync=0; }
    pthread_create(&ptid, NULL, producer, NULL);


    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);

    return 0;
}

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值