-4

What I want to realize is easily explained, but because there are so many different possibilities I'm not really aware of the pro and cons for each possible approach:

In my application there are plenty (say some thousands of communication objects). When such object is idle for some while (meaning that certain methods are not called), it shall simply be closed (what this means in detail is not relevant here).

I'm thinking of a "timer", associated with each object, which is re-triggered every time I "use" the object. Like:

public void ReTrigger()
{
    lock (_some_locking)
    {
            //Reset the timer
            _timer.Stop();
            _timer.Start();
        }
    }
}

Note, my application is heavily using async/await and I would like to use a solution which fits best into this concept. I want to avoid a lot of additional threads just for running a lot of timers.

There are many different timers available:

System.Timers.Timer
System.Threading.Timer
System.Windows.Forms.Timer
System.Web.UI.Timer
System.Windows.Threading.DispatcherTimer

So, which one "fits" best into my concept of using asyncio ?

To put an alternative, would it be better to rely on a background task like

while (true)
{
     try
     {
        await Task.Delay(timeout, _cancellationToken)
        ... some action when expired ...
     }
     catch (TaskCanceledException)
     {
        // we have been re-triggered by "using" the object
     }
}
       

This fits better in my concept, however, in this case I need a new cancellation token after each re-trigger, which is not really nice.

What is the "golden way" way to solve my problem; preferably using async tasks?

Another solution would be a housekeeping task, which cyclically polls all active objects for being expired or not. This would work with just one running timer, but is also not very nice.

MichaelW
  • 1,328
  • 1
  • 15
  • 32
  • 3
    cant you just store them in a memory cache with an expiration policy, and caoll update when used? – Bacon Jan 23 '21 at 22:19
  • 3
    When a communication object is closed, do you want something to happen at that moment? If yes, do you have any preference about the thread on which this something will be invoked? Also if yes, do you have any preference about the accuracy of the timer that will invoke the something? For example what about a resolution of [15 msec](https://stackoverflow.com/questions/3744032/why-are-net-timers-limited-to-15-ms-resolution)? Is it too accurate? Too inaccurate? Just right? – Theodor Zoulias Jan 23 '21 at 22:42
  • 1
    Have you looked up connection pooling? If you are using standard Framework classes, they sort out all of this for you. – Charlieface Jan 24 '21 at 01:58
  • 1
    The timer must not be accurately. If it is with an accuracy of some seconds it would be ok. When some object has not been used for, lets say 5 minutes, it shall be disposed in the background. – MichaelW Jan 24 '21 at 09:21
  • How many objects do you expect to have at any moment at maximum? Also how frequently are you going to call methods on an object that should restart its expiration timer, on average? – Theodor Zoulias Jan 24 '21 at 10:56

1 Answers1

1

Here is a way to keep track of the expiration status of an object passively, without using timers. The last time that each object was used is stored in a private double field, and this field is updated every time the object is used. In case the object has not been used for a long time, the field will take the value double.MaxValue which means "expired", and will keep this value forever. The GetExpired method below handles the complexity of comparing and updating the field atomically and with thread-safety:

public static bool GetExpired(ref double lastUsed, TimeSpan slidingExpiration,
    bool touch)
{
    // Magic values, 0: Not initialized, double.MaxValue: Expired
    double previous = Volatile.Read(ref lastUsed);
    if (previous == double.MaxValue) return true;

    // Get current timestamp in seconds
    double now = (double)Stopwatch.GetTimestamp() / Stopwatch.Frequency;
    if (previous == 0D || now - previous < slidingExpiration.TotalSeconds)
    {
        // Not expired (unless preempted)
        if (!touch) return false;
        var original = Interlocked.CompareExchange(ref lastUsed, now, previous);
        return original == double.MaxValue;
        // In any other case that original != previous we've lost the race to update
        // the field, but its value should be very close to 'now'. So not expired.
    }
    else
    {
        // Expired (unless preempted)
        var original = Interlocked.CompareExchange(ref lastUsed, double.MaxValue,
            previous);
        return original == double.MaxValue || original == previous;
    }
}

Usage example:

public class MyComObject
{
    private readonly TimeSpan _slidingExpiration = TimeSpan.FromSeconds(60);
    private double _lastUsed;

    public MyComObject() // Constructor
    {
        GetExpired(ref _lastUsed, default, touch: true); // Start expiration "timer"
    }

    public bool IsExpired => GetExpired(ref _lastUsed, _slidingExpiration, touch: false);

    public bool TryDoSomething()
    {
        if (GetExpired(ref _lastUsed, _slidingExpiration, touch: true)) return false;
        //...
        return true; // The job was done
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I understand this, but it is not what I actually need: In your proposal I have to call TryDoSomething to try some operation on the object and when it is expired, I will notice that from there. However I want to get rid of an object exactly when it hasn't been used for some while - so the mechanism must run automatically in the background. Of course polling is an option, but I want to avoid it. – MichaelW Jan 24 '21 at 09:15
  • 1
    @MichaelW hmmm. I think that the requirement for the timely disposal of expired objects is an important information, that should be included in the question. Could you edit the question and add it there? Btw check out the [expiration strategy](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory#additional-notes) of the quite popular `MemoryCache` component: *"Expiration doesn't happen in the background. There is no timer that actively scans the cache for expired items. Any activity on the cache (Get, Set, Remove) can trigger a background scan for expired items."* – Theodor Zoulias Jan 24 '21 at 10:33