C# 多线程编程 经典模型 生产者和消费者问题

语言:C#

总起:

在编写Unity程序的时候一般不用考虑到多线程的问题,但了解经典的三个问题对以后编写多线程会有所帮助,嘛,主要是好奇就研究看看。

多线程的两个主要的对象是Mutex互斥体和Semaphore信号量,当然现在语言都提供类似lock的关键字使用更加方便。不过既然C#提供了这样的实现,我还是以这两个对象作为展开。

Mutex互斥体和Semaphore信号量:

首先说明一下Mutex和Semaphore是内核级别的,对性能影响较大,而lock是基于Monitor的,所以普通情况下还是尽量使用lock。

♦ Mutex

互斥体可以让一段代码运行时只有一个线程占有,保证一个操作的原子性。

Mutex.WaitOne 尝试获取进入权限,如果已经有线程进入则等待,没有则进入后锁住;

Mutex.ReleaseMutex 释放权限。

♦ Semaphore

信号量其实和互斥体差不多,只不过它的内部使用int计数,比如说为5,相当于有5把钥匙,那么就是来一个线程就会取走一个,离开一个线程则会释放一个。如果碰到为0的情况,就需要等待。

只有0和1两种情况时其实就是互斥体。

Semaphore.WaitOne 尝试获取一份进入的权限;

Semaphore.Release 释放一份进入权限。

生产者和消费者的问题:

生产者生产物品放到一个Buffer中,由消费者从Buffer中取出物品并消费,从中产生的同步问题是最经典的一个同步模型(Web中从SQL中查改数据其实就是该问题)。

首先我们准备好数据和方法:

class Item
{
    public string name = "一个产品";
}

// 产品队列缓存
static Queue<Item> queue = new Queue<Item>();
static readonly int BUFFER_SIZE = 3;

// 同步标记
private static int ItemCount = 0;
static Semaphore fillCount = new Semaphore(0, BUFFER_SIZE);
static Semaphore emptyCount = new Semaphore(BUFFER_SIZE, BUFFER_SIZE);
static Mutex bufferMutex = new Mutex();

// 将产品放入缓存中
static void putItemIntoBuffer(Item item)
{
    queue.Enqueue(item);
    Console.WriteLine("将" + item.name + "放入队列,现在有" + queue.Count + "个");
}

// 从缓存中获取产品
static Item removeItemFromBuffer()
{
    var item = queue.Peek();
    queue.Dequeue();
    Console.WriteLine("将" + item.name + "取出队列,现在有" + queue.Count + "个");
    return item;
}

// 生产产品
static Item ProduceItem(int number)
{
    Thread.Sleep(TimeSpan.FromSeconds(1));
    Item item = new Item() {name = "产品" + number};
    Console.WriteLine("生产了"+item.name);
    return item;
}

// 消费产品
static void ConsumItem(Item item)
{
    Thread.Sleep(TimeSpan.FromSeconds(4));
    Console.WriteLine("消费了"+item.name);
}

static Thread producerThread;
static Thread consumerThread;

static void Main(string[] args)
{
    producerThread = new Thread(producer1);
    consumerThread = new Thread(consumer1);

    producerThread.Start();
    consumerThread.Start();
}

然后写两个线程方法,为了将问题暴露出来,我增加了判断if后阻塞15秒的语句。(挂起和恢复函数已经过时,现在不管怎么样都不应该在实际生产过程中使用该函数)


static void producer1()
{
    int productNumber = 0;
    while (true)
    {
        // 生产物品
        var item = ProduceItem(productNumber++);

        // 如果已经满了,则需要将现有线程挂起
        if (ItemCount == BUFFER_SIZE)
        {
            // 模拟判断完if后,消费者把所有缓存都消费了然后进入挂起阶段
            Thread.Sleep(TimeSpan.FromSeconds(15)); 
            Console.WriteLine("producerThread挂起");
            producerThread.Suspend();
        }

        // 将物品放入缓存
        putItemIntoBuffer(item);
        ItemCount = ItemCount + 1;

        // 如果有了一个物品,则唤醒消费者
        if (ItemCount == 1 && consumerThread.ThreadState == ThreadState.Suspended)
        {
            Console.WriteLine("consumerThread恢复");
            consumerThread.Resume();
        }
    }
}

static void consumer1()
{
    while (true)
    {
        // 如果没有物品,消费者挂起
        if (ItemCount == 0)
        {
            Console.WriteLine("consumerThread挂起");
            consumerThread.Suspend();
        }

        // 从缓存中移除一个物品
        var item = removeItemFromBuffer();
        ItemCount--;

        // 如果缓存中有了空缺则唤醒生产者
        if (ItemCount == BUFFER_SIZE - 1 && producerThread.ThreadState == ThreadState.Suspended)
        {
            Console.WriteLine("producerThread恢复");
            producerThread.Resume();
        }

        // 消费一个物品
        ConsumItem(item);
    }
}

以下是结果:



可以看到生产者生产满Buffer的产品后,判断了if,此时,消费者抢占了线程将所有物品全部消费,自身挂起的同时,生产者也挂起了。

这样就导致了死锁,也就是if语句没有原子性带来了后果,要解决这个问题,可以使用信号量的增减作为这边产品的增减。

其中fillCount是已经填了多少,而emptyCount是没有填的数量:

static void producer2()
{
    int productNumber = 0;
    while (true)
    {
        var item = ProduceItem(productNumber++);

        // 还有生产权限时,进入下面的代码
        emptyCount.WaitOne();
        // 将产品放入buffer中
        putItemIntoBuffer(item);
        // 释放一个拿去权限
        fillCount.Release();
    }
}

static void consumer2()
{
    while (true)
    {
        // 等待一个拿去权限
        fillCount.WaitOne();
        // 移除一个物品
        var item = removeItemFromBuffer();
        // 释放一个生产权限
        emptyCount.Release();
        ConsumItem(item);
    }
}

这样就成功解决了一个生产者和一个消费者同步的问题了。

以下是结果:



但是实际还是有一个问题,就是如果是多个生产者,或者多个消费者的情况,而放入和取出操作并不具有原子性,这样Buffer操作上就会出现问题。

比如是多个消费者的情况,第一个执行了queue.Peek(),但突然被中断了,第二个消费者也执行了queue.Peek(),那两个消费者取出的东西就是一样了,导致了错误。

这个解决方案就是同步对Buffer的操作,使其取出和添加时,不管做什么操作只允许一个线程,这边就直接使用互斥体了:

static void producer2()
{
    int productNumber = 0;
    while (true)
    {
        var item = ProduceItem(productNumber++);

        // 还有生产权限时,进入下面的代码
        emptyCount.WaitOne();
        bufferMutex.WaitOne();

        // 将产品放入buffer中
        putItemIntoBuffer(item);
        bufferMutex.ReleaseMutex();

        // 释放一个拿去权限
        fillCount.Release();
    }
}

static void consumer2()
{
    while (true)
    {
        // 等待一个拿去权限
        fillCount.WaitOne();
        bufferMutex.WaitOne();

        // 移除一个物品
        var item = removeItemFromBuffer();
        bufferMutex.ReleaseMutex();

        // 释放一个生产权限
        emptyCount.Release();
        ConsumItem(item);
    }
}

增加这样的互斥体其实也就是Web在操作数据库时采用的方法,其实查询数据倒还好一点,特别是增减数据这样的操作是最容易产生同步问题的。

个人:

接触多线程的时候还是一节windows课程,这节课的老师还自编了教材,当时不管怎么看都没懂信号量,今天一查wiki还有百度就懂了。回头又翻出了那本书看到了信号量的定义,嗯……还是没懂。

当时有个遗憾吧,最后上机大作业没认真做,回头看看能不能找到个题,再把它做一遍。


猜你喜欢

转载自blog.csdn.net/u012632851/article/details/78916745