操作系统之信号量(Semaphore)以及再C#中的应用,通过信号量实现资源共享,互斥锁,和线程同步操作。

.NET兼职社区

一、信号量是什么?

信号量(Semaphore) 是一种比互斥锁更强大的同步工具,它可以提供更高级的方法来同步并发进程或线程。它本质上是一个int类型的整数,除了初始化时候可以对他赋值之外,剩余的时间只能被标准的原子操作访问。而信号量的原子操作,一般就2个一个是P表示test另一个是V表示增加,简单点理解就是P内部执行等待和测试操作,V内部代码执行添加操作。可以通过信号量控制线程的并发同步操作,并且也可以通过信号量解决互斥锁的问题。下面将通过简单的例子分别介绍当信号量初始化值为0 OR >1 OR <0 OR =1的应用场景。当然信号量的应用场景很多,我这里只是简单举例。

这里给大家解释一下什么叫计算机的原子操作:原子操作你可以理解为代码块,当某个线程再执行原子操作的时候,它是不能够切换到其他线程去执行其他线程的任务的,只能等原子操作的代码块全部执行完成,操作系统才会切换到其他线程执行。

下面给大家简单看下信号量实现的伪代码:

P(s){
while(s<=0)
do nothing;
s--;
}
V(s)
{
s++;
}

P,V在操作系统中就是原子操作,每个对应的编程语言提供的原子操作方法可能不同,但是代码差不多。
上面代码S 表示信号量初始值。

二,使用信号量实现互斥锁功能(信号量=1的操作)。

 static Semaphore sema = new Semaphore(1, 1);
        const int cycleNum = 5;
        static void Main(string[] args)
        {
            for (int i = 0; i < cycleNum; i++)
            {
                Thread td = new Thread(testFuncation);
                td.Name = string.Format("编号{0}", i.ToString());
                td.Start(td.Name);

            }
            Console.Read();
        }

       public static void testFuncation(object obj)
        {
            sema.WaitOne();
            Console.WriteLine(obj.ToString() + "进入洗手间" + DateTime.Now.ToString());
            Thread.Sleep(2000);
            Console.WriteLine(obj.ToString() + "出洗手间" + DateTime.Now.ToString());
            sema.Release();
        }

在这里插入图片描述
重结果我们可以看出来,我们创建了5个线程,但是每次只能一个线程进入到我们的临界区,比如结果中,要么3号进了出来了其他的线程才可以进去。
使用信号量实现互斥锁的功能,我们只需要把信号量的初始化资源数量设置为1就行了。

顺便付上使用C#Mutex类实现互斥锁的代码:

    class Program
    {
        private static Mutex mutex = new Mutex();
        static void Main(string[] args)
        {
            Thread[] thread = new Thread[3];
            for (int i = 0; i < 3; i++)
            {
                thread[i] = new Thread(ThreadMethod1);//方法引用
                thread[i].Name = "Thread-" + (i + 1).ToString();
            }
            for (int i = 0; i < 3; i++)
            {
                thread[i].Start();
            }
            Console.ReadKey();
        }
        public static void ThreadMethod1(object val)
        {
            mutex.WaitOne();    //获取锁
            for (int i = 1; i <= 100; i++)
            {
                Console.WriteLine("{0}循環了{1}次", Thread.CurrentThread.Name, i);
            }
            mutex.ReleaseMutex();  //释放锁
        }
    }

三,使用信号量实现资源控制操作(信号量>1的操作)。


        static Semaphore sema = new Semaphore(5, 5);
        const int cycleNum = 9;
        static void Main(string[] args)
        {
            for (int i = 0; i < cycleNum; i++)
            {
                Thread td = new Thread(testFuncation);
                td.Name = string.Format("编号{0}", i.ToString());
                td.Start(td.Name);

            }
            Console.Read();
        }

代码几乎一样,只不过把信号量的初始化数量改成了5 ,结下来我们再看一下操作结果。
在这里插入图片描述
结果也可以很清楚的看到,每次最多进入5个线程,然后每次释放掉一个资源后,才会紧跟着再进入一个线程去操作临界区。

注:1.信号量的数量其实就等于资源可访问的数量、
    2.这里的原子操作,其实就是 sema.WaitOne();和 sema.Release();2个操作。每种语言的可能不同,但是原子操作的代码基本都一样,一个用于等待,一个用于添加。

四,使用信号量实现线程OR进程的同步(信号量=0的操作)。

这里拿司机开车和售票员售票来举例。
规则:
1.司机启动车辆之前必须等待售票员关闭车门
2.售票员开车门之前必须等待司机到站停车

using System;
using System.Threading;

namespace 信号量实现同步
{
    class Program
    {
        //用于控制司机的信号量
        private static Semaphore d = new Semaphore(0,1);
        //用于控制售票员的信号量
        private static Semaphore c = new Semaphore(0,1);


        //规则
     
        /// 1.司机启动车辆之前必须等待售票员关闭车门
        /// 2.售票员开车门之前必须等待司机到站停车
       
        //中间过程  正常行车和售票是互相不影响的  最终结果都不会变化


         
        static void Main(string[] args)
        {
            Thread[] threads = new Thread[2];
            threads[0] = new Thread(Diver);
            threads[0].Name = "司机操作";
            threads[1] = new Thread(Conducter);
            threads[1].Name = "售票员操作";
            foreach (var item in threads)
            {
                item.Start(item.Name);
            }
            Console.ReadKey();

        }

        static void Diver(object obj) 
        {
            d.WaitOne();  //让司机进入等待状态  只有关上车门了才可以开车
            Console.WriteLine($"{obj}:启动车辆!");

            Console.WriteLine($"{obj}:正常行车......");
            Thread.Sleep(2000);//假装车开了一会

            Console.WriteLine($"{obj}:到站停车!");

            c.Release();//通知司机到站了!

        }


        static void Conducter(object obj)
        {

            Console.WriteLine($"{obj}:关车门!");
            d.Release();//车门关上后 通知司机可以启动车了
            
            Console.WriteLine($"{obj}:售票......");
            Thread.Sleep(1000); //假装卖了会票

            c.WaitOne();//等待司机通知到站了

            Console.WriteLine($"{obj}:开车门!");

        }
    }
}

在这里插入图片描述
从结果可以很好看出,信号量很容易控制线程之间的同步,可以按照我们定义好的规则去执行。并不会随机执行指令、
代码比较简单,而且注释写的比较清楚,如果不懂,建议直接跑一下。
注:

1.实现信号量控制线程同步,我们只需要把初始值设置为0即可,这样可以直接让线程进入等待状态,直到另一个线程唤醒它。
2.同步问题实质上是将异步的(异步理解不了就理解为随机的,不受控的)并发线程按照某种顺序执行。
3.解决同步问题的本质就是找到并发进程的交互点,利用P操作的等待特点来调节进程的执行速度。

五,经典同步问题(生产者【P】和消费者【C】)。

在这里插入图片描述

5.1单缓存的解决方案。

using System;
using System.Threading;

namespace 生产者和消费者
{
    class Program
    {

        private static Semaphore _empty = new Semaphore(1,1);//生产者的信号量
        private static Semaphore _full = new Semaphore(0, 1);//消费者的信号量
        static void Main(string[] args)
        {
            Thread[] threads = new Thread[2];
            threads[0] = new Thread(Producer);
            threads[0].Name = "生产者";
            threads[1] = new Thread(Consumer);
            threads[1].Name = "消费者";
            foreach (var item in threads)
            {
                item.Start(item.Name);
            }

        }


        public static void Producer(object obj) 
        {
            while (true) 
            {
                Console.WriteLine($"{obj}:生产了一件商品!");
                _empty.WaitOne(); //检查缓存区是否有位置
                //如果有位置 就把产品放入缓存区
                Console.WriteLine($"{obj}:商品已放入缓冲区!");
                //通知消费者 有产品可以使用了
                _full.Release();
            
            }
        }

        public static void Consumer(object obj)
        {
            while (true)
            {
                _full.WaitOne();//等待  检查缓冲区是否有商品
                Console.WriteLine($"{obj}:在缓冲区中拿出商品!");
                _empty.Release();//通知生产者 缓存区没有产品了
                Console.WriteLine($"{obj}:消耗商品!");
            }
        }

    }
}

上面代码中,因为是单缓冲案例,所以我们把生产者初始值设为1,消费者初始信号量设为0,分别从2者角度去看,当生产者第一次生产出商品后,此时缓冲区值为1,就把当前产品放入缓存区,当P’来的时候,发现缓冲区目前是满的就会进行一个等待。而P这时候会通知消费者C进行消费了。然后我们站在消费者角度去观察,消费者最开始去拿商品,因为才开始缓冲区是没有商品的,所以初始值为0,此时消费者会进入一个忙等待的状态,当接受到P的信号后,就会重缓冲区拿出商品,然后通知生产者没有商品了,并消费。

5.2多缓存解决方案

先看下伪代码:
在这里插入图片描述

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace 生产者和消费者
{
   public class ManyThread
    {

    

        private static readonly int  _count = 4 ;
        private static string[] _strs = new string[_count];
        private static Semaphore _empty = new Semaphore(_count,_count);//生产
        private static Semaphore _full = new Semaphore(0, _count);//消费
        private static int _in, _out;
        private static Semaphore mutex = new Semaphore(1, 1);//定义一个互斥锁

        public static void Run() 
        {
            Thread [] Producters = new Thread[5];
            Thread[] Consumers = new Thread[5];
            for (int i = 0; i < 5; i++)
            {
                Producters[i] = new Thread(Producer);
            }

            for (int i = 0; i < 5; i++)
            {
                Consumers[i] = new Thread(Consumer);
            }

            for (int i = 0; i < 5; i++)
            {
                Producters[i].Start(Producters[i].ManagedThreadId);
                Consumers[i].Start(Producters[i].ManagedThreadId);
            }
        }


        public static void Producer(object obj)
        {
           
                Console.WriteLine($"{obj}:生产了一件商品!");
                _empty.WaitOne(); //检查缓存区是否有位置
                mutex.WaitOne();//上锁
                ///上锁和释放锁中间代码为临界区
                _strs[_in] = $"产品{_in}进入缓冲区";
                _in = (_in + 1) % _count;
                mutex.Release();//释放锁
                _full.Release();//通知消费者 有产品放入

         
        }

        public static void Consumer(object obj)
        {
           
                _full.WaitOne();//等待  检查缓冲区是否有商品
                mutex.WaitOne();//上锁
                string str = _strs[_out];
                _out = (_out + 1) % _count;
                mutex.Release();//释放锁
                
                _empty.Release();//通知生产者 缓存区没有产品了
                Console.WriteLine($"{obj}:消耗商品==》{str}");
           
        }


    }
}

注:    1.不要随意扩大临界区范围
        2.PV操作在同一线程的叫互斥信号量
        3.PV操作不在同一线程的叫同步信号量

六,经典同步问题(桔子苹果问题)。

在这里插入图片描述
来看下伪代码:
在这里插入图片描述

using System;
using System.Threading;

namespace 信号量苹果橘子解决方案
{
    class Program
    {
        //定义缓存区  盘子信号量
        public static Semaphore sp = new Semaphore(1, 1); //只允许放一个水果 初始值为1
        //定义不同水果的信号量
        public static Semaphore apple = new Semaphore(0, 1);//最开始盘子中没有苹果
        public static Semaphore orange = new Semaphore(0, 1); //盘子中没有橘子

        static void Main(string[] args)
        {
            //创建4个线程 分别代表爸爸 妈妈 儿子 女儿

            Thread father = new Thread((t)=> {
                while (true)
                {
                    Console.WriteLine($"{t} say:削一个苹果。");
                    sp.WaitOne(); //检查盘子是否有位置
                    Console.WriteLine($"{t} say:苹果已放入盘子......");
                    //通知女儿有苹果可以吃了
                    apple.Release();
                }
           
            });

            Thread mother = new Thread((t) => {
                while (true)
                {
                    Console.WriteLine($"{t} say:剥一个橘子。");
                    sp.WaitOne(); //检查盘子是否有位置
                    Console.WriteLine($"{t} say:橘子已放入盘子......");
                    //通知儿子有橘子可以吃了
                    orange.Release();
                }
            });

            Thread daughter = new Thread((t) => {

                while (true)
                {
                   
                    apple.WaitOne(); //检查盘子是否有苹果
                    Console.WriteLine($"{t} say:从盘子取出苹果......");
                    //释放盘子容量 告诉父亲没有苹果了
                    sp.Release();
                }
            });

            Thread son = new Thread((t) => {
                while (true)
                {
                    orange.WaitOne(); //检查盘子是否有橘子
                    Console.WriteLine($"{t} say:从盘子取出橘子......");
                    //释放盘子容量 告诉母亲没有橘子了
                    sp.Release();
                }
              
            });

            father.Name = "father";
            mother.Name = "mother";
            daughter.Name = "daughter";
            son.Name = "son";

            father.Start(father.Name);
            mother.Start(mother.Name);
            son.Start(son.Name);
            daughter.Start(daughter.Name);
        }
    }
}

在这里插入图片描述

这个例子很好的演绎了线程之间的协作和竞争关系,这样就很好的控制了线程之间的同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值