Something like this?
using Timer = System.Threading.Timer;
class Limiter{
public static readonly Limiter Instance = new Limiter();
Limiter(){}
int max;
Semaphore counter;
List<Timer> timers = new List<Timer>();
// warning: not thread safe!
public int MaxExecutionPerSecond{
get{return max;}
set{counter = new Semaphore(max = value, value);}
}
public void WaitIfRequired(){
// Our semaphore starts with a count of MaxExecutionPerSecond.
// When we call WaitOne(), it decrements the count. If the count
// is already zero, the call to WaitOne() will block until another
// thread calls Release() to increment the count.
counter.WaitOne();
// Set a timer to increment the semaphore in one second.
Timer t = null;
t = new Timer(o=>{
// Increment the semaphore.
counter.Release();
// We no longer need to protect this timer from the GC.
timers.Remove(t);
t.Dispose();
});
// Hold a reference to this timer to keep it from being disposed of.
timers.Add(t);
// Set the timer to release the semaphore in one second.
t.Change(1000, Timeout.Infinite);
}
}
EDIT
One thing to keep in mind is that the above code will only prevent many threads starting at once. If the threads are long running, then it's still possible to have many threads running at once. For instance, if you start 5 threads per second, but each thread runs for 1 second, then at any given time after 2 seconds, you'll theoretically have 10 threads running.
If you want to make sure you never have more than 5 threads running at once, the simplest thing to do is to dispense with the custom Limiter
class, and just use a Semaphore
directly.
const int maxThreadCount = 5;
static Semaphore counter = new Semaphore(maxThreadCount, maxThreadCount);
static void NewThread(object state){
counter.WaitOne();
// do something
counter.Release();
}
Now that's just about as simple as it can be. One caveat though: creating new threads and immediately sleeping them is generally considered a bad idea. This uses system resources to create a thread that's not doing anything. It's better to queue up requests and start up new threads (or better yet, use thread pool threads) to handle them only when they are eligible to run. This is more complicated and harder to get right. Depending on design, it might require an additional thread for a scheduler/manage. Something like this:
class Limiter{
class WorkData{
readonly ParameterizedThreadStart action;
readonly object data;
public ParameterizedThreadStart Action{get{return action;}}
public object Data {get{return data;}}
public WorkData(ParameterizedThreadStart action, object data){
this.action = action;
this.data = data;
}
}
readonly Semaphore threadCount;
readonly Queue<WorkData> workQueue = new Queue<WorkData>();
readonly Semaphore queueCount = new Semaphore(0, int.MaxValue);
public Limiter(int maxThreadCount){
threadCount = new Semaphore(maxThreadCount, maxThreadCount);
Thread t = new Thread(StartWorkItems);
t.IsBackground = true;
t.Start();
}
void StartWorkItems(object ignored){
while(queueCount.WaitOne() && threadCount.WaitOne()){
WorkData wd;
lock(workQueue)
wd = workQueue.Dequeue();
ThreadPool.QueueUserWorkItem(DoWork, wd);
}
}
void DoWork(object state){
WorkData wd = (WorkData)state;
wd.Action(wd.Data);
counter.Release();
}
public void QueueWork(ParameterizedThreadStart action, object data){
lock(workQueue)
workQueue.Enqueue(new WorkData(action, data));
queueCount.Release();
}
}
In this class, I've removed the singleton property and given the constructor a maxThreadCount
parameter. This avoids the lack of the thread safety in the property of the first class. There are other ways thread safety can be added, but this was the simplest.