So, first we look at the case you described when a method is always used from a user interface thread or some other synchronization context. The Run method may itself be async to handle all marshaling through the synchronization context for us.
If we run, we simply set the next saved action. If we do not, we will indicate that we are working now, waiting for the action, and then continue to wait for the next action until the next action. We guarantee that when we finish, we will indicate that we are finished working:
public class EventThrottler { private Func<Task> next = null; private bool isRunning = false; public async void Run(Func<Task> action) { if (isRunning) next = action; else { isRunning = true; try { await action(); while (next != null) { var nextCopy = next; next = null; await nextCopy(); } } finally { isRunning = false; } } } private static Lazy<EventThrottler> defaultInstance = new Lazy<EventThrottler>(() => new EventThrottler()); public static EventThrottler Default { get { return defaultInstance.Value; } } }
Since the class, at least, will usually be used exclusively from the user interface thread, there should usually be only one, so I added the default instance convenience property, but since it may still make sense to be more than one in the program, I did not his single.
Run accepts Func<Task> with the idea that in general it will be an asynchronous lambda. It might look like this:
public class Foo { public void SomeEventHandler(object sender, EventArgs args) { EventThrottler.Default.Run(async () => { await Task.Delay(1000);
Well, therefore, just to be detailed, here is a version that handles the case when event handlers are called from different threads. I know that you said that you think they are all called from the UI thread, but I generalized it a bit. This means blocking all access to the fields of the type instance in the lock block, but actually not executing the function inside the lock block. This last part is important not only for performance, to ensure that we do not block elements by simply setting the next field, but also to avoid problems with this action also triggering, so that it does not need to deal with re-solutions or possible deadlocks . This template, which makes the material in the lock block and then responds based on the conditions defined in the lock, sets local variables that indicate what should be done after the lock is completed.
public class EventThrottlerMultiThreaded { private object key = new object(); private Func<Task> next = null; private bool isRunning = false; public void Run(Func<Task> action) { bool shouldStartRunning = false; lock (key) { if (isRunning) next = action; else { isRunning = true; shouldStartRunning = true; } } Action<Task> continuation = null; continuation = task => { Func<Task> nextCopy = null; lock (key) { if (next != null) { nextCopy = next; next = null; } else { isRunning = false; } } if (nextCopy != null) nextCopy().ContinueWith(continuation); }; if (shouldStartRunning) action().ContinueWith(continuation); } }