Implementing a Starve method (“Unrelease”/“Hold”) for SemaphoreSlim

瘦欲@ 提交于 2021-01-28 18:41:49

问题


I'm using a SemaphoreSlim with a FIFO behaviour and now I want to add to it a Starve(int amount) method to remove threads from the pool, sort of the opposite to Release().

If there are any running tasks, they will of course continue until they are done, since for the moment the semaphore is not keeping track of what is actually running and "owes" the semaphore a release call.

The reason is that the user will dynamically control how many processes are allowed at any time for a given semaphore.

The strategy I'm following is:

  • if there are threads available, i.e., CurrentCount > 0, then call Await() on the SemaphoreSlim without releasing back.
  • if there are no more threads available, because presumably tasks are running and potentially even queuing, then next time that Release() is called ignore it to prevent threads being released (an int variable keeps count)

I have added the code I have so far below. The main issues I'm struggling with are how to ensure thread safety, no deadlocks and no surprising race conditions.

Given that I cannot access the private lock() of the semaphore, I created a new object to at least try and prevent several threads to manipulate the new variables (within the wrapper) at the same time.

However, I fear that other variables like CurrentCount which are within the SemaphoreSlim could also change half way through and mess things up... I would expect the lock in the Release() method to prevent changes to CurrentCount, but maybe I should also apply the lock to the Wait and WaitAsync (which potentially could also change CurrentCount)? That would probably also result in uneccessary locks between two calls to Wait (?)

The call to semaphore.Wait() is in this situation any better or worse than await semaphore.WaitAsync() ?

Are there any better ways to extend the functionality of a class such as SemaphoreSlim, which contains many private variables that potentially are needed or that would be useful to have access to?

I briefly considered creating a new class which inherits from SemaphoreSlim, or looking at extension methods, maybe using reflection to access the private variables,... but none seem to be obvious or valid.

    public class SemaphoreQueue
    {
        private SemaphoreSlim semaphore;
        private ConcurrentQueue<TaskCompletionSource<bool>> queue = new ConcurrentQueue<TaskCompletionSource<bool>>();

        private int releasesToIgnore;
        private object lockObj;
        private const int NO_MAXIMUM = Int32.MaxValue; // cannot access SemaphoreSlim.NO_MAXIMUM
        public SemaphoreQueue(int initialCount) : this(initialCount, NO_MAXIMUM) { }
        public SemaphoreQueue(int initialCount, int maxCount)
        {
            semaphore = new SemaphoreSlim(initialCount, maxCount);
            lockObj = new object();
            releasesToIgnore = 0;
        }
        public void Starve(int amount)
        {
            lock (lockObj)
            {
                // a maximum of CurrentCount threads can be immediatelly starved by calling Wait without release
                while ((semaphore.CurrentCount > 0) && (amount > 0))
                {
                    semaphore.Wait();
                    amount -= 1;
                }
                // presumably there are still tasks running. The next Releases will be ignored.
                if (amount > 0)
                    releasesToIgnore += amount;
            }
        }
        public int Release()
        {
            return Release(1);
        }
        public int Release(int num)
        {
            lock (lockObj)
            {
                if (releasesToIgnore > num)
                {
                    releasesToIgnore -= num;
                    return semaphore.CurrentCount;
                }
                else
                {
                    int oldReleasesToIgnore = releasesToIgnore;
                    releasesToIgnore = 0;
                    return semaphore.Release(num - oldReleasesToIgnore);
                }
            }
        }
        public void Wait(CancellationToken token)
        {
            WaitAsync(token).Wait();
        }
        public Task WaitAsync(CancellationToken token)
        {
            var tcs = new TaskCompletionSource<bool>();
            queue.Enqueue(tcs);
            QueuedAwait(token);
            return tcs.Task;
        }
        public int CurrentCount { get => this.semaphore.CurrentCount; }
        private void QueuedAwait(CancellationToken token)
        {
            semaphore.WaitAsync(token).ContinueWith(t =>
            {
                TaskCompletionSource<bool> popped;
                if (queue.TryDequeue(out popped))
                    popped.SetResult(true);
            });
        }

        public void Dispose()
        {
            semaphore.Dispose();
        }
    }

回答1:


I think that implementing a custom semaphore on top of the SemaphoreSlim class is problematic, because we don't have access to the synchronization primitives used by the built-in implementation. So I would suggest to implement it using solely a queue of TaskCompletionSource objects. Below is a basic implementation with missing features. The WaitAsync method lacks cancellation, and the Release method lacks the releaseCount argument as well.

For simplicity a releasesToIgnore counter is not used, and instead the existing currentCount is allowed to have negative values. The Starve method just decreases this counter.

public class SemaphoreFifo
{
    private readonly Queue<TaskCompletionSource<bool>> _queue
        = new Queue<TaskCompletionSource<bool>>();
    private readonly object _locker = new object();
    private readonly int _maxCount;
    private int _currentCount;

    public SemaphoreFifo(int initialCount, int maxCount)
    {
        _currentCount = initialCount;
        _maxCount = maxCount;
    }
    public SemaphoreFifo(int initialCount) : this(initialCount, Int32.MaxValue) { }

    public int CurrentCount { get { lock (_locker) return _currentCount; } }

    public async Task WaitAsync()
    {
        TaskCompletionSource<bool> tcs;
        lock (_locker)
        {
            if (_currentCount > 0)
            {
                _currentCount--;
                return;
            }
            tcs = new TaskCompletionSource<bool>();
            _queue.Enqueue(tcs);
        }
        await tcs.Task;
    }

    public void Starve(int starveCount)
    {
        lock (_locker) _currentCount -= starveCount;
    }

    public void Release()
    {
        TaskCompletionSource<bool> tcs;
        lock (_locker)
        {
            if (_currentCount < 0)
            {
                _currentCount++;
                return;
            }
            if (_queue.Count == 0)
            {
                if (_currentCount >= _maxCount) throw new SemaphoreFullException();
                _currentCount++;
                return;
            }
            tcs = _queue.Dequeue();
        }
        tcs.SetResult(true);
    }
}


来源:https://stackoverflow.com/questions/59431431/implementing-a-starve-method-unrelease-hold-for-semaphoreslim

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!