一、信号量是什么?
信号量(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);
}
}
}
这个例子很好的演绎了线程之间的协作和竞争关系,这样就很好的控制了线程之间的同步。