【问题标题】:AutoResetEvent and multiple SetsAutoResetEvent 和多个 Set
【发布时间】:2012-01-22 06:44:40
【问题描述】:

我正在尝试围绕堆栈设计一个数据结构,该结构会阻塞直到堆栈有可用的项目。我尝试使用AutoResetEvent,但我想我误解了同步过程的工作原理。基本上,看下面的代码,当没有可用的东西时,我试图从堆栈中弹出。

似乎AutoResetEvent 的行为就像一个信号量。那是对的吗?我可以摆脱BlockingStack.Get() 中的Set() 并完成它吗?或者这会导致我只使用我的堆栈项之一。

public class BlockingStack
{
    private Stack<MyType> _internalStack;
    private AutoResetEvent _blockUntilAvailable;

    public BlockingStack()
    {
        _internalStack = new Stack<MyType>(5);
        _blockUntilAvailable = new AutoResetEvent(false);

        for (int i = 0; i < 5; ++i)
        {
            var obj = new MyType();
            Add(obj);
        }
    }

    public MyType Get()
    {
        _blockUntilAvailable.WatiOne();

        lock (_internalStack)
        {
            var obj = _internalStack.Pop();
            if (_internalStack.Count > 0)
            {
                _blockUntilAvailable.Set(); // do I need to do this?
            }

            return obj;
        }
    }

    public void Add(MyType obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);
            _blockUntilAvailable.Set();
        }
    }
}

我的假设是当WaitOne() 函数调用通过时所有等待线程的AutoResetEvent 重置。但是,似乎有多个线程正在进入。除非我在某个地方搞砸了我的逻辑。

编辑:这是给 Silverlight 的。

【问题讨论】:

标签: c# data-structures concurrency stack autoresetevent


【解决方案1】:

我没有验证基于 Monitor 的解决方案,但我确实编写了一个似乎正在工作的基于信号量的解决方案:

public class Semaphore
{
    private int _count;
    private int _maximum;
    private object _countGuard;

    public Semaphore(int maximum)
    {
        _count = 0;
        _maximum = maximum;
        _countGuard = new object();
    }

    public void WaitOne()
    {
        while (true)
        {
            lock (_countGuard)
            {
                if (_count < _maximum)
                {
                    _count++;
                    return;
                }
            }
            Thread.Sleep(50);
        }
    }

    public void ReleaseOne()
    {
        lock (_countGuard)
        {
            if (_count > 0)
            {
                _count--;
            }
        }
    }
}

public class BlockingStack
{
    private Stack<MyType> _internalStack;
    private Semaphore _blockUntilAvailable;

    public BlockingStack()
    {
        _internalStack = new Stack<MyType>(5);
        _blockUntilAvailable = new Semaphore(5);

        for (int i = 0; i < 5; ++i)
        {
            var obj = new MyType();
            Add(obj);
        }
    }

    public MyType Get()
    {
        _blockUntilAvailable.WaitOne();

        lock (_internalStack)
        {
            var obj = _internalStack.Pop();
            return obj;
        }
    }

    public void Add(MyType obj)
    {
        lock (_internalStack)
        {
            _internalStack.Push(obj);
            _blockUntilAvailable.ReleaseOne();
        }
    }
}

【讨论】:

    【解决方案2】:

    不,您当前的代码没有意义。目前,每次调用 Get 方法(.WaitOnecall)时,您都会阻塞线程。

    你可能想要这样的东西:

    public class BlockingStack<T>
    {
        private Stack<T> _internalStack;
        private AutoResetEvent _blockUntilAvailable;
    
        public BlockingStack()
        {
            _internalStack = new Stack<T>(5);
            _blockUntilAvailable = new AutoResetEvent(false);
        }
    
        public T Pop()
        {
            lock (_internalStack)
            {
                if (_internalStack.Count == 0)
                    _blockUntilAvailable.WaitOne();
    
                return _internalStack.Pop();
            }
        }
    
        public void Push(T obj)
        {
            lock (_internalStack)
            {
                _internalStack.Push(obj);
    
                if(_internalStack.Count == 0)
                    _blockUntilAvailable.Set();
            }
        }
    }
    

    这个想法是,如果_internalStack 中的当前项目数为0,那么它应该等待来自Push 方法的信号。一旦收到信号,它就会继续前进并从堆栈中弹出一个项目。


    编辑: 上面的代码有两个问题:

    1. 每当Pop.WaitOne 阻塞时,它都不会释放锁定 _internalStack,因此Push 永远无法获得锁。

    2. Pop在同一个线程上被多次调用时,它们共享 AutoResetEvent 的相同 initialState - 例如。推送信号 AutoResetEvent 添加项目时。现在当我弹出一个项目时 第一次工作正常,因为实际上有一个项目 Stack。但是第二次,Stack 没有任何价值,所以 它通过在AutoResetEvent 上调用.WaitOne 来等待 - 但因为 对 Push 的调用表示此事件,它只会返回 true,并且 没有按预期等待。

    一个(有效的)替代方案:

    public class BlockingStack<T>
    {
        private Stack<T> _internalStack;
    
        public BlockingStack()
        {
            _internalStack = new Stack<T>(5);
        }
    
        public T Pop()
        {
            lock (_internalStack)
            {
                if (_internalStack.Count == 0)
                    Monitor.Wait(_internalStack);
    
                return _internalStack.Pop();
            }
        }
    
        public void Push(T obj)
        {
            lock (_internalStack)
            {
                _internalStack.Push(obj);
                Monitor.Pulse(_internalStack);
            }
        }
    }
    

    【讨论】:

    • 不行,调用Push,Pop,Pop,Push,观察死锁。
    • @HansPassant,为什么要在单个线程上调用 Push、Pop、Pop、Push?尽管 Pop 阻塞了线程,你是否希望它以某种方式在同一个线程上调用 Push?
    • @HansPassant,请查看更新,看看我这次是否正确:)
    • 注意我链接的帖子中的while循环,当有多个消费者线程时需要。
    • @HansPassant,我不知道你为什么想要一个 while 循环......你能详细说明一下吗?
    【解决方案3】:

    除非您只是想了解线程的工作原理,否则最好使用阻塞集合。这将为您提供一个由堆栈支持的阻塞集合:

    ConcurrentStack<SomeType> MyStack = new ConcurrentStack<SomeType>();
    BlockingCollection<SomeType> SharedStack = new BlockingCollection<SomeType>(MyStack)
    

    然后,您可以以线程安全的方式访问它,并为您正确完成所有阻塞。见here

    您可以通过调用sharedStack.Take() 来使用sharedStack,然后它会阻止获取,直到有东西可以从堆栈中获取。


    编辑: 花了我一段时间(和两次尝试),但我认为我已经解决了你的问题。

    考虑一个有 3 个线程等待事件的空堆栈。

    调用add,栈有一个对象,允许一个线程通过事件。

    立即再次调用 Add。

    第一个线程现在等待从 Add 获取锁。

    Add 将第二个对象添加到堆栈并让另一个线程通过该事件。

    现在堆栈上有两个对象和两个线程通过事件,都在等待锁。

    First Get 线程现在需要锁定并弹出。仍然看到堆栈上的一个对象并调用 SET。

    事件允许第三个线程。

    Second Get 线程现在需要锁定并弹出。在堆栈中什么都看不到,也不调用 set。

    但是。太晚了。第三个线程已经被允许通过,所以当第二个线程放弃锁时,第三个线程尝试从空堆栈中弹出并抛出。

    【讨论】:

    • 这不能回答 OP 问题 - 我正在尝试围绕堆栈设计一个数据结构,该结构会阻塞直到堆栈有可用的项目。
    • 不,不是特别 - 这就是为什么我注意到他/她可能只是想了解正在发生的事情。如果他们实际上是在尝试创建这个结构来使用,但更好的选择是使用阻塞集合(如果使用 .net4)。创建阻塞集合很难做到正确,而且很容易以微妙的方式出错。
    • 这正是阻塞集合的作用。你在它上面调用 Take() ,它会阻塞直到有东西可以从底层集合中获取。
    • silverlight 真可惜——BlockingCollection 真的很有用。实际上,我只是在盯着你的代码看了一会儿之后才运行你的代码,而不是想看看当堆栈为空时你怎么能拿东西。对我来说很好。你有例外吗?就目前而言,在第一次添加某些内容之前,您将无法从堆栈中检索初始值,并且 AutoResetEvents 因没有按照您的预期执行而臭名昭著,但它似乎应该可以工作。
    • 呃。意识到我第一次没有完全正确。实际上,我不得不起床思考这个问题,因为它一直在我脑海里转来转去。这次我肯定有。我希望如此,真的可以睡一会儿:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多