Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to ensure a method logic is executed only once per arguments combination?

I'm designing a class library that have a bunch of method of kind "EnsureXXX". The idea of this methods is to be called whenever a calling code requires something than can requires an initialization specific to the arguments. It's similar the EnsureChildControls method of ASP.Net, but with arguments as discriminators.

Ex:

public static class SomeUtilityClass {
    public static void EnsureSomething(string arg1, int arg2, object arg3)
    {
        // Logic should be called once for each args combination 
    }
}

public class CallerClass
{
    public void Foo()
    {
        SomeUtilityClass.EnsureSomething("mycustomerid", 4, myData.SomeProperty);
    }
    public void Foo2()
    {
        SomeUtilityClass.EnsureSomething("mycustomerid", 4, myData.SomeProperty);
    }

}

As such pattern will be reuse at several places and called very often, I have to keep performance as a target. I also have to have a thread-safe method.

For this purpose, I wrote a small utility class :

public sealed class CallHelper
{
    private static readonly HashSet<int> g_YetCalled = new HashSet<int>();
    private static readonly object g_SyncRoot = new object();

    public static void EnsureOnce(Type type, Action a, params object[] arguments)
    {
        // algorithm for hashing adapted from http://stackoverflow.com/a/263416/588868
        int hash = 17;
        hash = hash * 41 + type.GetHashCode();
        hash = hash * 41 + a.GetHashCode();
        for (int i = 0; i < arguments.Length; i++)
        {
            hash = hash * 41 + (arguments[i] ?? 0).GetHashCode();
        }

        if (!g_YetCalled.Contains(hash))
        {
            lock (g_SyncRoot)
            {
                if (!g_YetCalled.Contains(hash))
                {
                    a();
                    g_YetCalled.Add(hash);
                }
            }
        }
    }
}

The consuming code looks like this :

public static class Program
{
    static void Main()
    {
        SomeMethod("1", 1, 1);
        SomeMethod("2", 1, 1);
        SomeMethod("1", 1, 1);
        SomeMethod("1", 1, null);

        Console.ReadLine();
    }

    static void SomeMethod(string arg1, int arg2, object arg3)
    {
        CallHelper.EnsureOnce(typeof(Program), ()=>
        {
            Console.WriteLine("SomeMethod called only once for {0}, {1} and {2}", arg1, arg2, arg3);
        }, arg1, arg2, arg3);
    }
}

The output is, as expected :

SomeMethod called only once for 1, 1 and 1
SomeMethod called only once for 2, 1 and 1
SomeMethod called only once for 1, 1 and

I have some questions related to this approach :

  1. I think I have properly locked the class to ensure thread safety, but am I right ?
  2. Is HashSet<int> and my method of computing the hash correct ? I'm especially wondering if the null handling is correct, and if I can "hash" an Action delegate this way.
  3. My methods currently supports only static methods. How can I move to a instance compatible method (adding the instance as a discriminator) without memory leaking?
  4. Is there any way to avoid passing all arguments manually to the utility method (just specifying the action), without exploring the stacktrace (because of the performance impact) ? I fear a lot of bugs introduced because of a missing arguments from the outermethod.

Thanks in advance

like image 241
Steve B Avatar asked Jan 12 '12 13:01

Steve B


1 Answers

This is essentially a memoize, except your function is void. But the same considerations for comparison of the input arguments are still valid.

Wes Dyer discusses how to make a generic, multi-argument memoize in this post. The general idea is to roll all the arguments into an anonymous type, and use that as the dictionary key.

Regarding making this thread-safe, consider that .NET already has concurrent collections. You don't need to make a non-concurrent collection into a concurrent one; just use the collections provided.

To make this work for instance methods without leaking, either keep a WeakReference to the instance or store an instance of the memoizer in the instance itself, whichever works for you.

like image 116
Craig Stuntz Avatar answered Sep 28 '22 07:09

Craig Stuntz