Linux基于线程安全处理的条件变量代码解析和举例

1. 条件量基本概念

在许多场合中,程序的执行通常需要满足一定的条件,条件不成熟的时候,任务应该进入睡眠阻塞等待,条件成熟时应该可以被快速唤醒。另外,在并发程序中,会其他任务同时访问该条件,因此任何时候都必须以互斥的方式对条件进行访问。条件量就是专门解决上述场景的逻辑机制。
注意,上述表述中,条件和条件量是两个不同的东西,所谓条件就是指程序要继续运行所需要的前提条件,比如文件是否读完、内存是否清空等具体的场景限定,而条件量(即pthread_cond_t)是本节课件要讨论的一种同步互斥变量,专用于解决上述逻辑场景。
条件量的逻辑
在这里插入图片描述

说明:
在进行条件判断前,先加锁(防止其他任务并发访问)
成功加锁后,判断条件是否允许
若条件允许,则直接操作临界资源,然后释放锁
若条件不允许,则进入条件量的等待队列中睡眠,并同时释放锁
在条件量中睡眠的任务,可以被其他任务唤醒,唤醒时重新判定条件是否允许程序继续执行,当然也是必须先加锁。

2. 条件量的使用

条件量一般要跟互斥锁(或二值信号量)配套使用,互斥锁提供锁住临界资源的功能,条件量提供阻塞睡眠和唤醒的功能。
一般流程示例
以取款为例,假设有多个任务可同时访问存款余额 balance,其中某个任务希望从中取出 ¥100 元,并且要求满足如下逻辑:
如果余额中有大于等于100元,则立即取出
如果余额小于100元,则进入睡眠等待
当有别的任务修改了余额时可被唤醒,并继续判定是否可取款

pthread_mutex_t m;  // 互斥锁
pthread_cond_t  v;  // 条件量

// 银行余额(全局变量,意味着有别的进程可随时访问)

extern int balance;
int main()
{
    // 1,初始化
    pthread_mutex_init(&m, NULL);
    pthread_cond_init(&v, NULL);

    // 2,对m加锁
    pthread_mutex_lock(&m);
    // 2,当条件不允许时,进入条件量中睡眠
    //    进入睡眠时,会自动对m解锁
    //    退出睡眠时,会自动对m加锁
    while(balance < 100)
        pthread_cond_wait(&v, &m);

    // 3,取款
    balance -= 100;

    // 4,对m解锁
    pthread_mutex_unlock(&m);
}
其他任务,可以在适当的时候,通过如下接口来唤醒处于睡眠态的任务:
// 单个唤醒,唤醒第一个进入条件量中睡眠的任务
pthread_cond_signal(&v);
// 集体唤醒,唤醒进入条件量中睡眠的所有任务
pthread_cond_broadcast(&v);

3. 死锁概念

死锁指的是由于某种逻辑问题,导致等待一把永远无法获得的锁的困境。比如最简单的是同一线程,连续对同一锁资源进行加锁,就进入了死锁。
死锁

最简单的死锁示例

pthread_mutex_t m;
int main()
{
    pthread_mutex_init(&m, NULL);
    // 正常加锁
    pthread_mutex_lock(&m);
    // 未释放锁前重复加锁,进入死锁状态
    pthread_mutex_lock(&m);
    // 下面的代码永远无法执行
    ...
    ...
}

以上死锁的例子,可以通过仔细检查代码得以避免,但在现实场景中,有些产生死锁的情况是无法避免的,比如如下情形:
一条线程持有一把锁,期间不能屏蔽 取消 指令 然后又恰巧被取消指令强制终止,此时死锁的产生变得不可避免。
产生死锁示例

void *routine(void *arg)
{
	thread_pool *pool = (thread_pool *)arg;
	struct task *p;
	while(1)
	{
        // 操作临界资源之前,加锁
		pthread_mutex_lock(&pool->lock);
        // 条件不允许时,进入条件量等待
		while(pool->waiting_tasks == 0 && !pool->shutdown)
			pthread_cond_wait(&pool->cond, &pool->lock);

        // 条件允许时,操作临界资源
		p = pool->task_list->next;
		pool->task_list->next = p->next;
		pool->waiting_tasks--;

        // !!! 注意 !!!
        // 线程若恰好在此处被意外终止,将导致死锁
        // 解锁
		pthread_mutex_unlock(&pool->lock);

        // 其他操作
		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
		(p->do_task)(p->arg);
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
		free(p);
	}
	pthread_exit(NULL);
}

上述代码中,若线程在中间被取消,则导致死锁。对于这种情况,一个可行的解决办法是:
提前准备一个解锁处理函数,并将其压入线程专用的函数栈中备用。
准备操作临界资源,加锁
操作临界资源
重点:
若线程在此期间意外终止,则会自动调用处理函数解锁

4.解锁概念

在函数栈中弹出处理函数。
说明:
上述做法实际上相当于现实生活中的立遗嘱,因为人去世之后是无法再做任何事情的,因此为了防止死亡在关键阶段意外到来,可以在提前立遗嘱,万一不幸遇到该情况就有了预案(处理函数),但如果并未发生此种情形,那么就将遗嘱作废(弹出处理函数且不执行)即可。
根据以上思路,可将上述代码改良为如下代码: 最完美的步骤
// 意外处理函数:
// 自动解锁

void handler(void *arg)
{
	pthread_mutex_unlock((pthread_mutex_t *)arg);
}
void *routine(void *arg)
{
	thread_pool *pool = (thread_pool *)arg;
	struct task *p;
	while(1)
	{
		//================================================//
		pthread_cleanup_push(handler, (void *)&pool->lock); // 提前准备好意外处理函数
		pthread_mutex_lock(&pool->lock);
		//================================================//
		// 1, no task, and is NOT shutting down, then wait
		while(pool->waiting_tasks == 0 && !pool->shutdown)
			pthread_cond_wait(&pool->cond, &pool->lock);

		// 2, no task, and is shutting down, then exit
		if(pool->waiting_tasks == 0 && pool->shutdown == true)
		{
			pthread_mutex_unlock(&pool->lock);
			pthread_exit(NULL); // CANNOT use 'break';
		}

		// 3, have some task, then consume it
		p = pool->task_list->next;
		pool->task_list->next = p->next;
		pool->waiting_tasks--;

		//================================================//
		pthread_mutex_unlock(&pool->lock); 
		pthread_cleanup_pop(0); // 弹出处理函数且不执行
		//================================================//

		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
		(p->do_task)(p->arg);
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

		free(p);
	}
	pthread_exit(NULL);
}

注意:

pthread_cleanup_push() 用于将处理函数填入栈中,在线程意外终止后会被自动调用。
pthread_cleanup_pop() 用于将栈中的处理函数弹出,若参数为0则意味着不执行,参数不为零则意味着执行该函数。
pthread_cleanup_push() 和 pthread_cleanup_pop() 必须成对出现。

示例代码:pthread_cleanup.c

综合举例:

编写一个多线程程序,让其中一条线程扮演父母(parent),其余线程扮演子女(chilren)。
父母和所有子女共有一个银行账号,可同时对余额存取款。
父母可随时往账户存入款项,并通过条件量通知子女。
子女可随时从账户取出款项,但必须保证账户数额正确、安全。
余额不可为负数。
取款数额最少是 ¥100 元。

解答
利用条件量和互斥锁,协同各个线程
参考代码

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

// 银行余额
int balance = 0;

pthread_mutex_t m;
pthread_cond_t v;

// 辅助线程:显示程序流逝时间
void *count_times(void *args)
{
	int i = 1;
	while(1)
	{
		sleep(1);
		fprintf(stderr, "second: %d\n", i++);
	}

	pthread_exit(NULL);
}

void *routine(void *args)
{
	/* ====================== */
	pthread_mutex_lock(&m);
	/* ====================== */

	/*
	** NOTE: whenever pthread_cond_wait() returns, it
	** indicates that the calling thread is holding
	** the mutex. So we need to unlock the mutex.
	*/
	while(balance < 100)
		pthread_cond_wait(&v, &m);

	fprintf(stderr, "t%d: balance = %d\n", (int)args, balance);
	balance -= 100;

	/* ====================== */
	pthread_mutex_unlock(&m);
	/* ====================== */

    pthread_exit(NULL);
}

int main(int argc, char **argv)
{
	if(argc != 2)
	{
		printf("请指定子女线程数量\n");
        exit(0);
	}

	pthread_mutex_init(&m, NULL);
	pthread_cond_init(&v, NULL);

	pthread_t tid;
	pthread_create(&tid, NULL, count_times, NULL);


	/* create a set of threads */
	int i, thread_nums = atoi(argv[1]);
	for(i=0; i<thread_nums; i++)
		pthread_create(&tid, NULL, routine, (void *)i);

	/*
	** sleep(1) makes sure that the thread-routine
	** will lock the mutex first, then condition variable
	** cause the thread to be suspended.
	**
	** sleep(3) will indicate that pthread_cond_wait()
	** won't return even if the condition varialble has been
	** changed. Thus, the condition variable is NOT associated
	** with a specified condition.
	** 
	** Actually, pthread_cond_wait() calling threads will
	** be waken up ONLY IF someone calls pthread_cond_signal()
	** or pthread_cond_broadcast() and mutex is accessable.
	** therefore, pthread_cond_wait() calling thread should
	** test critical source again after pthread_cond_wait()
	** returned, because it may be changed by anther thread
	** which access the mutex first
	*/
	sleep(1);
	pthread_mutex_lock(&m);

	balance += 300;
	/*
	** pthread_cond_signal() will wake up the first thread in
	** the condition waiting-queue, while pthread_cond_broadcast()
	** wake up all threads which are waiting in the queue.
	**
	** NOTE: pthread_cond_signal() and pthread_cond_broadcast()
	** won't wake up the waiting thread immediately, it only
	** wake up one(or them) in the condition-variable waiting
	** queue.
	**
	** NOTE: POSIX doesn't require to own mutex before the calling
	** thread calls pthread_cond_signal() or pthread_cond_broadcast()
	*/

	pthread_cond_broadcast(&v);
	//pthread_cond_signal(&v);
	sleep(3);

	pthread_mutex_unlock(&m);

	pthread_exit(NULL);

	/*
	** exit() or return in main() will cause the whole thread group
	** terminate.
	*/
	// return 0;
}

POSIX信号量(不管是匿名的还是具名的)都是单个定义和操作的,因此POSIX信号量只能协同单一的资源,但单一的资源进行 P/V 操作,而systemV的信号量组,可以同时对一组资源同时原子性地进行 P/V 操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Qt历险记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值