Bootstrap

浅谈进程同步和互斥的概念

简介

    进程同步是一个操作系统级别的概念,是在多道程序的环境下,存在着不同的制约关系,为了协调这种互相制约的关系,实现资源共享和进程协作,从而避免进程之间的冲突,引入了进程同步。

 

临界资源

    在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。

    对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区

    对于临界区的访问过程分为四个部分:

    1.进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞

    2.临界区:在临界区做操作

    3.退出区:清除临界区被占用的标志

    4.剩余区:进程与临界区不相关部分的代码

 

进程间同步和互诉的概念

 

进程同步

    进程同步也是进程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系来源于他们之间的合作。

    比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒。概念如图1所示。

    1

     图1.进程之间的同步

 

     用C#代码模拟进程之间的同步如代码1所示。

 class ProcessSyn
    {
        private static Mutex mut = new Mutex();
        
        
        static void Main()
        {
            Console.WriteLine("进程1执行完了进程2才能执行.......");
            Thread Thread1 = new Thread(new ThreadStart(Proc1));
            Thread Thread2 = new Thread(new ThreadStart(Proc2));
            Thread1.Start();
            Thread2.Start();
            Console.ReadKey();   
        }
       
        private static void Proc1()
        {
            mut.WaitOne();
            Console.WriteLine("线程1执行操作....");
            Thread.Sleep(3000);
            mut.ReleaseMutex();//V操作

        }
        private static void Proc2()
        {
            

            mut.WaitOne();//P操作
            Console.WriteLine("线程2执行操作....");
            mut.WaitOne();
        }
    }

    代码1.C#模拟进程之间的同步

 

    运行结果如图2所示。

    2

    图2.运行结果

 

进程互斥

    进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。只有当使用临界资源的进程退出临界区后,这个进程才会解除阻塞状态。

    比如进程B需要访问打印机,但此时进程A占有了打印机,进程B会被阻塞,直到进程A释放了打印机资源,进程B才可以继续执行。概念如图3所示。

    3

     图3.进程之间的互斥

 

     用C#模拟进程之间的互斥,这里我启动了5个线程,但同一时间内只有一个线程能对临界资源进行访问。如代码2所示。

class ProcessMutex
    {
        private static Mutex mut = new Mutex();
        private const int numThreads = 5;
        
        static void Main()
        {
            
            for (int i = 0; i <= numThreads; i++)
            {
                Thread myThread = new Thread(new ThreadStart(UseResource));
                myThread.Name = String.Format("线程{0}", i + 1);
                myThread.Start();
            }
            Console.ReadKey();
            
        }
        
        
        //同步
        private static void UseResource()
        {
            // 相当于P操作
            mut.WaitOne();

            
            /*下面代码是线程真正的工作*/
            Console.WriteLine("{0}已经进入临界区",
                Thread.CurrentThread.Name);
            Random r = new Random();
            int rNum = r.Next(2000);
            
            Console.WriteLine("{0}执行操作,执行时间为{1}ms", Thread.CurrentThread.Name,rNum);
            Thread.Sleep(rNum);

            Console.WriteLine("{0}已经离开临界区\r\n",
                Thread.CurrentThread.Name);
            /*线程工作结束*/


            
            // 相当于V操作
            mut.ReleaseMutex();
        }
        //互斥
  


    }

    代码2.C#模拟进程之间的互斥

 

     运行结果如图4所示。

    4   

    图4.C#模拟进程互斥

 

实现临界区互斥的基本方法

 硬件实现方法

    通过硬件实现临界区最简单的办法就是关CPU的中断。从计算机原理我们知道,CPU进行进程切换是需要通过中断来进行。如果屏蔽了中断那么就可以保证当前进程顺利的将临界区代码执行完,从而实现了互斥。这个办法的步骤就是:屏蔽中断--执行临界区--开中断。但这样做并不好,这大大限制了处理器交替执行任务的能力。并且将关中断的权限交给用户代码,那么如果用户代码屏蔽了中断后不再开,那系统岂不是跪了?

    还有硬件的指令实现方式,这个方式和接下来要说的信号量方式如出一辙。但是通过硬件来实现,这里就不细说了。

 

信号量实现方式

    这也是我们比较熟悉P V操作。通过设置一个表示资源个数的信号量S,通过对信号量S的P和V操作来实现进程的的互斥。

    P和V操作分别来自荷兰语Passeren和Vrijgeven,分别表示占有和释放。P V操作是操作系统的原语,意味着具有原子性。

    P操作首先减少信号量,表示有一个进程将占用或等待资源,然后检测S是否小于0,如果小于0则阻塞,如果大于0则占有资源进行执行。

    V操作是和P操作相反的操作,首先增加信号量,表示占用或等待资源的进程减少了1个。然后检测S是否小于0,如果小于0则唤醒等待使用S资源的其它进程。

    前面我们C#模拟进程的同步和互斥其实算是信号量进行实现的。

 

一些经典利用信号量实现同步的问题

生产者--消费者问题

    问题描述:生产者-消费者问题是一个经典的进程同步问题,该问题最早由Dijkstra提出,用以演示他提出的信号量机制。本作业要求设计在同一个进程地址空间内执行的两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中获得物品,然后释放缓冲区。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来

 

    这里生产者和消费者是既同步又互斥的关系,首先只有生产者生产了,消费着才能消费,这里是同步的关系。但他们对于临界区的访问又是互斥的关系。因此需要三个信号量empty和full用于同步缓冲区,而mut变量用于在访问缓冲区时是互斥的。

    利用C#模拟生产者和消费者的关系如代码3所示。

    class ProducerAndCustomer
    {
        //临界区信号量
        private static Mutex mut = new Mutex();

        private static Semaphore empty = new Semaphore(5, 5);//空闲缓冲区
        private static Semaphore full = new Semaphore(0, 5);
        //生产者-消费者模拟
         static void Main()
         {
             Console.WriteLine("生产者消费者模拟......");
             for (int i = 1; i < 9; i++)
             {
                 Thread Thread1 = new Thread(new ThreadStart(Producer));
                 Thread Thread2 = new Thread(new ThreadStart(Customer));
                 Thread1.Name = String.Format("生产者线程{0}", i);
                 Thread2.Name = String.Format("消费者线程{0}", i);
                 Thread1.Start();
                 Thread2.Start();
             }
             Console.ReadKey();

         }
         
         private static void Producer()
         {
             Console.WriteLine("{0}已经启动",Thread.CurrentThread.Name);
             empty.WaitOne();//对empty进行P操作
             mut.WaitOne();//对mut进行P操作
             Console.WriteLine("{0}放入数据到临界区", Thread.CurrentThread.Name);
                 Thread.Sleep(1000);
             mut.ReleaseMutex();//对mut进行V操作
             full.Release();//对full进行V操作
         }
         private static void Customer()
         {
             Console.WriteLine("{0}已经启动", Thread.CurrentThread.Name);
             Thread.Sleep(12000);
             full.WaitOne();//对full进行P操作
             mut.WaitOne();//对mut进行P操作
             Console.WriteLine("{0}读取临界区", Thread.CurrentThread.Name);
             mut.ReleaseMutex();//对mut进行V操作
             empty.Release();//对empty进行V操作
         }
    }

   代码3.使用C#模拟生产者和消费者的关系

 

    运行结果如图5所示。

    5

     图5.生产者消费者C#模拟结果

 

读者--写者问题

    问题描述:

      一个数据文件或记录,统称数据对象,可被多个进程共享,其中有些进程只要求读称为"读者",而另一些进程要求写或修改称为"写者"。

      规定:允许多个读者同时读一个共享对象,但禁止读者、写者同时访问一个共享对象,也禁止多个写者访问一个共享对象,否则将违反Bernstein并发执行条件。

    通过描述可以分析,这里的读者和写者是互斥的,而写者和写者也是互斥的,但读者之间并不互斥。

    由此我们可以设置3个变量,一个用来统计读者的数量,另外两个分别用于对读者数量读写的互斥,读者和读者写者和写者的互斥。如代码4所示。

class ReaderAndWriter
    {
        private static Mutex mut = new Mutex();//用于保护读者数量的互斥信号量
        private static Mutex rw = new Mutex();//保证读者写者互斥的信号量

        static int count = 0;//读者数量
        
        
        static void Main()
        {
            Console.WriteLine("读者写者模拟......");
            for (int i = 1; i < 6; i++)
            {

                Thread Thread1 = new Thread(new ThreadStart(Reader));
                Thread1.Name = String.Format("读者线程{0}", i);
                Thread1.Start();
                
            }
            Thread Thread2 = new Thread(new ThreadStart(writer));
            Thread2.Name = String.Format("写者线程");
            Thread2.Start();
            Console.ReadKey();


        }
        
        private static void Reader()
        {
            
            mut.WaitOne();
            if (count == 0)
            {
                rw.WaitOne();
            }
            count++;
            mut.ReleaseMutex();
            
            Thread.Sleep(new Random().Next(2000));//读取耗时1S
            Console.WriteLine("读取完毕");
            
            mut.WaitOne();
            count--;
            mut.ReleaseMutex();
            if (count == 0)
            {
                rw.ReleaseMutex();
            }

        }
        private static void writer()
        {
            
            rw.WaitOne();
            Console.WriteLine("写入数据");
            rw.ReleaseMutex();
    
        }

   代码4.C#模拟读者和写者问题

 

    运行结果如图6所示。

    

    图6.读者写者的运行结果

 

哲学家进餐问题

    问题描述:

    有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。

    8

    图7.哲学家进餐问题

 

    根据问题描述,五个哲学家分别可以看作是五个进程。五只筷子分别看作是五个资源。只有当哲学家分别拥有左右的资源时,才得以进餐。如果不指定规则,当每个哲学家手中只拿了一只筷子时会造成死锁,从而五个哲学家都因为吃不到饭而饿死。因此我们的策略是让哲学家同时拿起两只筷子。因此我们需要对每个资源设置一个信号量,此外,还需要使得哲学家同时拿起两只筷子而设置一个互斥信号量,如代码5所示。

class philosopher
    {
        private static int[] chopstick=new int[5];//分别代表哲学家的5只筷子
        private static Mutex eat = new Mutex();//用于保证哲学家同时拿起两双筷子
        static void Main()
        {
            //初始设置所有筷子可用
            for (int k = 1; k <= 5; k++)
            {
                chopstick[k - 1] = 1;
            }
            //每个哲学家轮流进餐一次
            for(int i=1;i<=5;i++)
            {
                Thread Thread1 = new Thread(new ThreadStart(Philosophers));
                Thread1.Name = i.ToString();
                Thread1.Start();
            }
            Console.ReadKey();
        }
        private static void Philosophers()
        {
            
            //如果筷子不可用,则等待2秒
            while (chopstick[int.Parse(Thread.CurrentThread.Name)-1] !=1 || chopstick[(int.Parse(Thread.CurrentThread.Name))%4]!=1)
            {
                Console.WriteLine("哲学家{0}正在等待", Thread.CurrentThread.Name);
                Thread.Sleep(2000);
            }
            eat.WaitOne();
            //同时拿起两双筷子
            chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 0;
            chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 0;
            eat.ReleaseMutex();
            Thread.Sleep(1000);
            Console.WriteLine("哲学家{0}正在用餐...",Thread.CurrentThread.Name);
            //用餐完毕后放下筷子
            chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 1;
            chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 1;
            Console.WriteLine("哲学家{0}用餐完毕,继续思考", Thread.CurrentThread.Name);
        }
    }

    代码5.C#模拟哲学家用餐问题

 

    运行结果如图7所示。

    7

    图8.哲学家问题运行结果

 

 

总结

    本文介绍了进程的同步和互斥的概念,临界区的概念,以及实现进程同步互斥的方式,并解决了3种实现同步的经典问题,并给出了相应的C#模拟代码。操作系统对于进程的管理是是计算机编程的基础之一,因此掌握这个概念会使你的内功更上一层:-D

;