linux基础——经典线程同步问题解析及编程实现

前两天写了个简单的线程池,结果在处理线程的同步互斥上花了不少时间,觉得有必要把以前学习的知识再过一遍,这次主要复习的是几个非常经典的同步互斥问题。

一、生产者消费者问题
问题描述:
只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
问题分析
1、关系分析:生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,它们也是同步关系。
2、整理思路:因为只有生产者和消费者两个线程,且这两个线程存在着互斥关系和同步关系。那么需要解决的是互斥和同步的PV操作的位置。
3、信号量设置:互斥锁mutex用于控制互斥访问缓冲池;信号量full用于记录当前缓冲池中“满”缓冲区数,初值为 0;信号量empty用于记录当前缓冲池中“空”缓冲区数,初值为n。
#define N 10
sem_t empty;//空闲缓冲区信号量,初始化为N
sem_t full;//满缓冲区信号量,初始化为0
pthread_mutex_t mutex;//对缓冲区产品互斥访问
int product_num = 0;//缓冲区产品数目

void *producter_f(void *arg)
{
        while (1)
        {
                usleep(1000000);
                printf("produce an item\n");//生产数据
                sem_wait(&empty);//获取空缓冲区单元
                pthread_mutex_lock(&mutex);//进入临界区
                product_num++;//将数据放入缓冲区
                printf("producter: the num of product is %d\n", product_num);
                pthread_mutex_unlock(&mutex);//离开临界区
                sem_post(&full);//满缓冲区加1
        }
}

void *consumer_f(void *arg)
{
        while (1)
        {
                sem_wait(&full);//获取空缓冲区单元
                pthread_mutex_lock(&mutex);//进入临界区
                product_num--;
                printf("consumer: the num of product is %d\n", product_num);
                pthread_mutex_unlock(&mutex);//离开临界区
                sem_post(&empty);//空缓冲区加1
                usleep(5000000);
                printf("consume an item\n");//消费数据
        }
}
运行结果:
linux基础——经典线程同步问题解析及编程实现_第1张图片
该类问题要注意对缓冲区大小为n的处理,当缓冲区中有空时便可对empty变量执行P 操作,一旦取走一个产品便要执行V操作以释放空闲区。对empty和full变量的P操作必须放在对mutex的P操作之前。如果生产者线程先执行P(mutex),然后执行P(empty),消费者执行P(mutex),然后执行P(fall),这样可不可以?答案是否定的。设想生产者线程已经将缓冲区放满,消费者线程并没有取产品,即empty = 0,当下次仍然是生产者线程运行时,它先执行P(mutex)封锁信号量,再执行P(empty)时将被阻塞,希望消费者取出产品后将其唤醒。轮到消费者线程运行时,它先执行P(mutex),然而由于生产者线程已经封锁mutex信号量,消费者线程也会被阻塞,这样一来生产者、消费者线程都将阻塞,都指望对方唤醒自己,陷入了无休止的等待。同理,如果消费者线程已经将缓冲区取空,即 full = 0,下次如果还是消费者先运行,也会出现类似的死锁。不过生产者释放信号量时,mutex、full先释放哪一个无所谓,消费者先释放mutex还是empty都可以。

二、读者-写者问题
问题描述:
有读者和写者两组并发线程,共享一个文件,当两个或以上的读线程同时访问共享数据时不会产生副作用,但若某个写线程和其他线程(读线程或写线程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。
问题分析:
1) 关系分析。由题目分析读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥问题。
2) 整理思路。两个线程,即读者和写者。写者是比较简单的,它和任何线程互斥,用互斥信号量的P操作、V操作即可解决。读者的问题比较复杂,它必须实现与写者互斥的同时还要实现与其他读者的同步,因此,仅仅简单的一对P操作、V操作是无法解决的。那么,在这里用到了一个计数器,用它来判断当前是否有读者读文件。当有读者的时候写者是无法写文件的,此时读者会一直占用文件,当没有读者的时候写者才可以写文件。同时这里不同读者对计数器的访问也应该是互斥的。
3) 信号量设置。首先设置信号量count为计数器,用来记录当前读者数量,初值为0; 设置mutex为互斥锁,用于保护更新count变量时的互斥;设置互斥信号量rw用于保证读者和写者的互斥访问。
sem_t rw;//保护读者和写者互斥地访问文件,初始化为1
pthread_mutex_t mutex;//保护更新reader_num时的互斥
int reader_num = 0;//用于记录读者数量

void *writer_f(void *arg)
{
        sem_wait(&rw);//互斥访问共享文件
        usleep(2000000);
        printf("writing ...\n");
        sem_post(&rw); //释放共享文件
}

void *reader_f(void *arg)
{
        pthread_mutex_lock(&mutex);//互斥访问reader_num变量

        if (reader_num == 0)
        {
            sem_wait(&rw);//阻止写线程写
        }
        reader_num++;//读者计数器加1
        printf("reader num add the num is %d \n", reader_num);
        pthread_mutex_unlock(&mutex);//释放互斥变量


        usleep(2000000);
        printf("reading ...\n");

        pthread_mutex_lock(&mutex);//互斥访问reader_num变量

        reader_num--;//读者计数器减1
        printf("reader num dec the num is %d \n", reader_num);

        if (reader_num == 0)
        {
                sem_post(&rw);//允许写线程写
        }

        pthread_mutex_unlock(&mutex);//释放互斥变量
}
运行结果:
linux基础——经典线程同步问题解析及编程实现_第2张图片
在上面的算法中, 读线程是优先的,也就是说,当存在读线程时,写操作将被延迟,并且只要有一个读线程活跃,随后而来的读线程都将被允许访问文件。这样的方式下,会导致写线程可能长时间等待,且存在写线程“饿死”的情况。
如果希望写线程优先,即当有读线程正在读共享文件时,有写线程请求访问,这时应禁止后续读线程的请求,等待到已在共享文件的读线程执行完毕则立即让写线程执行,只有在无写线程执行的情况下才允许读线程再次运行。为此,增加一个信号量并且在上面的程序中 writer_f()和reader_f()函数中各增加一对PV操作,就可以得到 写线程优先的解决程序。
sem_t rw;//保护读者和写者互斥地访问文件,初始化为1
sem_t w;//用于实现写优先,初始化为1
pthread_mutex_t mutex;//保护更新reader_num时的互斥
int reader_num = 0;//用于记录读者数量

void *writer_f(void *arg)
{
        sem_wait(&w);
        sem_wait(&rw);//互斥访问共享文件
        usleep(2000000);
        printf("writing ...\n");
        sem_post(&rw); //释放共享文件
        sem_post(&w);
}

void *reader_f(void *arg)
{
        sem_wait(&w);
        pthread_mutex_lock(&mutex);//互斥访问reader_num变量

        if (reader_num == 0)
        {
            sem_wait(&rw);//阻止写线程写
        }
        reader_num++;//读者计数器加1
        printf("reader num add the num is %d \n", reader_num);
        pthread_mutex_unlock(&mutex);//释放互斥变量
        sem_post(&w);

        usleep(2000000);
        printf("reading ...\n");

        pthread_mutex_lock(&mutex);//互斥访问reader_num变量

        reader_num--;//读者计数器减1
        printf("reader num dec the num is %d \n", reader_num);

        if (reader_num == 0)
        {
                sem_post(&rw);//允许写线程写
        }

        pthread_mutex_unlock(&mutex);//释放互斥变量
}
linux基础——经典线程同步问题解析及编程实现_第3张图片

三、哲学家进餐问题
问题描述:
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭,如图2-10所示。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
问题分析:
1) 关系分析。5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
2) 整理思路。显然这里有五个线程。本题的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象。那么解决方法有两个,一个是让他们同时拿两个筷子;二是对每个哲学家的动作制定规则,避免饥饿或者死锁现象的发生。
3) 信号量设置。定义互斥信号量数组chopstick[5] = {l, 1, 1, 1, 1}用于对5个筷子的互斥访问。
对哲学家按顺序从0~4编号,哲学家i左边的筷子的编号为i,哲学家右边的筷子的编号为(i+l)%5。
该算法存在以下问题:当五个哲学家都想要进餐,分别拿起他们左边筷子的时候(都恰好执行完wait(chopstick[i]);)筷子已经被拿光了,等到他们再想拿右边的筷子的时候(执行 wait(chopstick[(i+l)%5]);)就全被阻塞了,这就出现了死锁。
linux基础——经典线程同步问题解析及编程实现_第4张图片
为了防止死锁的发生,可以对哲学家线程施加一些限制条件,比如至多允许四个哲学家同时进餐;仅当一个哲学家左右两边的筷子都可用时才允许他抓起筷子;对哲学家顺序编号,要求奇数号哲学家先抓左边的筷子,然后再转他右边的筷子,而偶数号哲学家刚好相反。正解制定规则如下:假设釆用第二种方法,当一个哲学家左右两边的筷子都可用时,才允许他抓起筷子。
sem_t chopstick[N];//定义信号量数组,初始化为1
pthread_mutex_t mutex;//保护取筷子时的互斥
int reader_num = 0;//用于记录读者数量

void *philosopher_i(void *arg)
{
        int index = *(int *)arg;

        pthread_mutex_lock(&mutex);//在取筷子前获得互斥量
        sem_wait(&chopstick[index]);//取左边筷子
        sem_wait(&chopstick[(index+1) % 5]);//取右边筷子
        printf("get two chopstick the index is %d\n", index);
        pthread_mutex_unlock(&mutex);//释放取筷子的互斥量
        usleep(2000000);
        printf("eating... the index is %d\n", index);//进餐

        sem_post(&chopstick[index]);//放回左边筷子
        sem_post(&chopstick[(index+1) % 5]);//放回右边筷子
        usleep(2000000);        
        printf("thinking... the index is %d\n", index);//思考
}
运行结果:
linux基础——经典线程同步问题解析及编程实现_第5张图片

四、吸烟者问题
问题描述:
假设一个系统有三个抽烟者线程和一个供应者线程。每个抽烟者不停地卷烟 并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟 者中,第一个拥有烟草、第二个拥有纸,第三个拥有胶水。供应者线程无限地提供三种材料, 供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供 应者一个信号告诉完成了,供应者就会放另外两种材料在桌上,这种过程一直重复(让三个 抽烟者轮流地抽烟)。
问题分析:
1) 关系分析。供应者与三个抽烟者分别是同步关系。由于供应者无法同时满足两个或 以上的抽烟者,三个抽烟者对抽烟这个动作互斥(或由三个抽烟者轮流抽烟得知
2) 整理思路。显然这里有四个线程。供应者作为生产者向三个抽烟者提供材料。
3) 信号量设置。信号量offer1、offer2、offer3分别表示烟草和纸组合的资源、烟草和 胶水组合的资源、纸和胶水组合的资源。信号量finish用于互斥进行抽烟动作。
sem_t offer1;//定义信号量对应烟草和纸组合的资源,初始化为1
sem_t offer2;//定义信号量对应烟草和胶水组合的资源,初始化为1
sem_t offer3;//定义信号量对应纸和胶水组合的资源,初始化为1
sem_t finish;//定义信号量表示吸烟者是否完成,初始化为1

void *provider_f(void *arg)
{
        while (1)
        {
                int rand_num = rand() % 3;
                if (rand_num == 0)
                {
                        sem_post(&offer1);//提供烟草和纸
                }
                else if (rand_num == 1)
                {
                        sem_post(&offer2);//提供烟草和胶水
                }
                else
                {
                        sem_post(&offer3);//提供纸和胶水
                }

                usleep(1000000);
                printf("put two material on table\n");

                sem_wait(&finish);
        }
}

void *smoker1_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer1);
                usleep(1000000);
                printf("remove the paper and glue\n");//拿走纸和胶水
                sem_post(&finish);
        }   
}

void *smoker2_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer2);
                usleep(1000000);
                printf("remove the tobacco and glue\n");//拿走烟草和胶水
                sem_post(&finish);
        }   
}

void *smoker3_f(void *arg)
{
        while (1)
        {
                sem_wait(&offer3);
                usleep(1000000);
                printf("remove the paper and tobacco\n");//拿走纸和烟草
                sem_post(&finish);
        }   
}
运行结果:
linux基础——经典线程同步问题解析及编程实现_第6张图片

参考:
http://c.biancheng.net/cpp/html/2600.html


你可能感兴趣的:(linux)