哲学家就餐问题的总结

关于本文

前几天在完成操作系统实验的时候,选择的是“哲学家就餐”和“进程间通信”两个实验。
本文主要总结一下在“哲学家就餐”实验过程中遇到的一些问题以供他人或者我以后参考。

正文

  1. 互斥量的初始化

    这个东西我一开始就理解吧,因为那个互斥量是个结构,在没有初始化的时候里面的值全都是0或者不可预料的数据,而实际的互斥量里面的数据都是有规定的,什么值代表什么意义,所以那些0或者不可预料的数据对于互斥量来说是无意义的,所以必须先初始化,初始化的过程就是把这个互斥量结构赋值成“未锁定”的状态。但在初始化的时候遇到了一些小问题,互斥量的初始化有两种方式,一种是静态初始化,一种是动态初始化。

    静态初始化如下:

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化函数如下:

    int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

我犯的第一个错误就是,声明一个互斥量全局数组以后,而在函数中,使用静态初始化的方法对其进行初始化,其结果就是编译器无情地报错。后来折腾了一下才知道,静态初始化互斥量的这种方式只能在声明互斥量的时候才能使用,这也是为什么叫“静态初始化”的原因。如果要在程序运行过程中初始化某个互斥量,就必须使用动态初始化的方式。
  1. 关于pthread_create

    其实在之前计算机网络的作业——编写HTTP服务端的时候,我接下来要说的这个问题西就已经被我发现了,但在做“哲学家就餐“这个实验的时候,我再次犯了这个错误(我在网上搜索了一下,一些网上的示例代码都有这个问题,我不得不汗颜,貌似很多程序员都不注重细节额。。),而且一开始还被程序执行结果误导,在其它地方找了很久的错误,最终才回想起来这个问题。

    问题的具体描述如下:

    void* phi(void* param)
    {
        int i = *(int*)param;
        while (1)
        {
            think(i);
            take_forks(i);
            eat(i);
            put_forks(i);
        }
        return NULL;
    }
    int main()
    {
        srand((unsigned) time(NULL));
        init_mutex();
        int i;
        pthread_t tid[N];
        for (i = 0; i < N-1; i++)
        {
            pthread_create(&tid[i], NULL, phi, (void*)(&i));
        }
        phi(&i);
        return 0;
    }

上面这个程序片段中,在main函数中的for循环和phi函数那里有一个很大的BUG,而且这个BUG很特殊,它可能在某些计算机上会出现,但也可能在某些计算机上不出现,它的出现可能取决于这台计算机的运行速度,也可能取决于这台计算机的线程调度程序。

接下来我详细描述一下这个BUG,上面那个程序片段的for循环中,通过pthread_create创建N-1个哲学家线程(剩下的第N个哲学家线程是由主线程直接调用哲学家函数产生),问题出在pthread_create这个函数的最后一个参数,这个参数的值是按地址传递,这会造成什么后果呢?想象一下,如果创建哲学家线程以后,由于线程调度的原因,哲学家函数中的那句

    int i = *(int*)param;

还没来得及执行,而主线程里面的for循环就进入了第二次循环,也就是说执行了一次i++,那么就造成哲学家函数中获取到的i值就不是当初创建这个线程时的i值,最糟糕的情况下,这个i值可能相差很远,比如主线程里面for循环连续循环几次以后,第一个哲学家线程才被调度,这个时候获取到的i值可能已经很大了,早已不是当初的创建第一个哲学家线程时的0了。

我看到有的人在解决这个问题的时候采用的方法是:
在创建哲学家线程之后加入一句:

    sleep(1);

好了,这个方法“或许”管用,为什么说或许呢,因为它至少减少了出现上面的那种情况的概率,但我说的只是减少概率,但并不能从根本上解决这个问题,为什么?如果这台计算机上已经有很多线程在并发运行了,即便你加入了一个sleep函数,让其挂起,但有可能,当它唤醒的时候,之前创建的那个线程还是没得到调度,也就是说还是没来得及获取i值,于是问题还是出现了,这还是依赖于这个操作系统的线程调度算法。
那么如何从根本上解决这个问题呢?我采用的是互斥量,虽然这个实验是用互斥量解决“哲学家就餐”问题本身,但顺带也可以用互斥量解决这个线程同步问题。

    void* phi(void* param)
    {
        int i = *(int*)param;
        pthread_mutex_unlock(&mutex);
        while (1)
        {
            think(i);
            take_forks(i);
            eat(i);
            put_forks(i);
        }
        return NULL;
    }
    int main()
    {
        srand((unsigned) time(NULL));
        init_mutex();
        int i;
        pthread_t tid[N];
        for (pthread_mutex_lock(&mutex),i = 0; i < N-1; i++)
        {
            pthread_create(&tid[i], NULL, phi, (void*)(&i));
            pthread_mutex_lock(&mutex);
        }
        phi(&i);
        return 0;
    }

这个是修改以后的代码片段,我加入了一个名为mutex的互斥量,这个互斥量在我进入for循环体之前lock了一下,然后在创建线程后面再次lock了一下,由创建的哲学家线程执行完

    int i = *(int*)param;

这个获取i值的语句以后对其进行unlock操作。通过这种方法,保证了for循环中的i++操作一定是在哲学家线程获取完i值之后才进行,进而保证了哲学家线程获取的i值一定是当时创建该哲学家线程的时候的i值。

好了本篇文章到此结束,我觉得没必要把我写的哲学家就餐问题的源代码全贴上了,我说了这篇文章只是一个简单总结,确切来说是对我在写解决“哲学家就餐”问题这个程序中遇到的问题的总结,至于“哲学家就餐”问题本身并不在本文的阐述范围内。

标签: none

添加新评论