I have a method RefreshCustomersInternal
that refreshes customer data from a server and returns a Task
. Since this process is very costly, I want to prevent this method from being called too many times in a short time or prevent the process from running if there's another refresh process running already. So a wrapper method RefreshCustomers
is written for this purpose:
private Task _refreshTask;
private readonly object _refreshLock = new object();
private DateTime _lastRefreshTime = DateTime.MinValue;
private static readonly TimeSpan MinimumRefreshInterval = TimeSpan.FromSeconds(5);
private Task RefreshCustomers()
{
lock (_refreshLock)
{
// If there is already a refresh task, just return this one.
if (_refreshTask != null)
return _refreshTask;
// Check if the time between this refresh request and the previous one is too close.
var intervalFromLastRefresh = DateTime.Now - _lastRefreshTime;
if (intervalFromLastRefresh < MinimumRefreshInterval)
{
// If this refresh request is too close to the previous one,
// make this request wait until the minimum refresh interval is passed,
// then perform the refresh.
var waitTaskSource = new TaskCompletionSource<bool>();
_refreshTask = waitTaskSource.Task.ContinueWith(_=>RefreshCustomersInternal()).Unwrap();
// Force the wait in a background thread to ensure a non-blocking UI experience.
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.Sleep(MinimumRefreshInterval - intervalFromLastRefresh);
waitTaskSource.SetResult(true);
});
}
else
// If this refresh request is far enough from the last one,
// just perform the normal refresh.
_refreshTask = RefreshCustomersInternal();
// When the refresh process is done, always clear the refresh task reference
// and update the refresh timestamp.
return _refreshTask = _refreshTask.ContinueWith(task =>
{
lock (_refreshLock)
{
_refreshTask = null;
_lastRefreshTime = DateTime.Now;
}
});
}
}
In a previous version of this method, instead of using the ThreadPool
, it was simply just calling the Thread.Sleep
method in a new Task
:
if (intervalFromLastRefresh < MinimumRefreshInterval)
_refreshTask = Task.Factory.StartNew(() =>
{
Thread.Sleep(MinimumRefreshInterval - internvalFromLastRefresh);
return RefreshCustomersInternal();
}).Unwrap();
Since there is no guarantee that Task.Factory.StartNew
will never start the new task on the UI thread, I updated it to use the ThreadPool
to force the wait into a background thread.
The method seems working from some basic tests, but I wonder if there is any better way for the part where I had to use the ThreadPool.QueueUserWorkItem
to force the wait in a background thread?
BTW, the code runs in .NET 4.0 hence the lack of all the neat async
and await
statements.
RefreshCustomersInternal
look like? Have tried refreshing once, waiting half the interval and then spamming refresh requests? It looks to me like you'd let extra refreshes in. \$\endgroup\$WebRequest
to download data from server and saving them into a local DB, which takes a few seconds. TheRefreshCustomers
method works because the first line in the lock checks if there is already a_refreshTask
and returns it if there is. This ensures that all calls to this method will get the sameTask
instance before the task is set tonull
which only happens after the real refresh process is finished. And because the code sets_refreshTask
uses the same lock, it guarantees thread safety of the variable. \$\endgroup\$waitTaskSource.Task.ContinueWith
is assigned to_refreshTask
in the same scope of the first lock, andwaitTaskSource
is only set to finish when the background wait is elapsed. \$\endgroup\$