14

Here on stack overflow I've found the code that memoizes single-argument functions:

static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var d = new Dictionary<A, R>();
    return a=> 
    {
        R r;
        if (!d.TryGetValue(a, out r))
        {
            r = f(a);
            d.Add(a, r);
        }
        return r;
    };
}

While this code does its job for me, it fails sometimes when the memoized function is called from the multiple threads simultaneously: the Add method gets called twice with the same argument and throws an exception.

How can I make the memoization thread-safe?

Community
  • 1
  • 1
Gman
  • 1,781
  • 1
  • 23
  • 38

4 Answers4

24

You can use ConcurrentDictionary.GetOrAdd which does everything you need:

static Func<A, R> ThreadsafeMemoize<A, R>(this Func<A, R> f)
{
    var cache = new ConcurrentDictionary<A, R>();

    return argument => cache.GetOrAdd(argument, f);
}

The function f should be thread-safe itself, because it can be called from multiple threads simultaneously.

This code also doesn't guarantee that function f is called only once per unique argument value. It can be called many times, in fact, in the busy environment. If you need this kind of contract, you should take a look at the answers in this related question, but be warned that they're not as compact and require using locks.

Community
  • 1
  • 1
Gman
  • 1,781
  • 1
  • 23
  • 38
  • 6
    Note that `GetOrAdd` will not completely prevent f being called more than once for a given argument; it only guarantees that the result of just *one* of the invocations is added to the dictionary. You can get more than one invocation in the event of threads checking the cache concurrently before the cached value has been added. It's often not worth worrying about this, but I mention it in case invocation has unwanted side-effects. – James World Dec 13 '13 at 10:51
  • @JamesWorld Yep, that's right. Edited answer to reflect that, thank you! – Gman Dec 13 '13 at 12:24
  • 1
    I'm a bit confused - isn't `cache` here a local variable? Each time `ThreadsafeMemoize()` is called, won't it create a new dictionary? – dashnick Aug 14 '17 at 16:23
  • @dashnick yes, but you supply a function and get a function back. So to memoize each function, you need to call `ThreadsafeMemoize` only once and then use the function that is returned in place of the function that you've put in. This way the cache is created once per memoized function. – Gman Aug 15 '17 at 05:33
  • If you want to memoize a function with more than one argument, you might be interested in [my answer below](https://stackoverflow.com/a/60721449/98422). – Gary McGill Mar 17 '20 at 11:28
2

Expanding on GMan's answer, I wanted to memoize a function with more than one argument. Here's how I did it, using a C# Tuple (requires C# 7) as they key for the ConcurrentDictionary.

This technique could easily be extended to allow yet more arguments:

public static class FunctionExtensions
{
    // Function with 1 argument
    public static Func<TArgument, TResult> Memoize<TArgument, TResult>
    (
        this Func<TArgument, TResult> func
    )
    {
        var cache = new ConcurrentDictionary<TArgument, TResult>();

        return argument => cache.GetOrAdd(argument, func);
    }

    // Function with 2 arguments
    public static Func<TArgument1, TArgument2, TResult> Memoize<TArgument1, TArgument2, TResult>
    (
        this Func<TArgument1, TArgument2, TResult> func
    )
    {
        var cache = new ConcurrentDictionary<(TArgument1, TArgument2), TResult>();

        return (argument1, argument2) =>
            cache.GetOrAdd((argument1, argument2), tuple => func(tuple.Item1, tuple.Item2));
    }
}

For example:

Func<int, string> example1Func = i => i.ToString();
var example1Memoized = example1Func.Memoize();
var example1Result = example1Memoized(66);

Func<int, int, int> example2Func = (a, b) => a + b;
var example2Memoized = example2Func.Memoize();
var example2Result = example2Memoized(3, 4);

(Of course, to get the benefit of memoization you'd normally want to keep example1Memoized / example2Memoized in a class variable or somewhere where they're not short-lived).

Gary McGill
  • 26,400
  • 25
  • 118
  • 202
1

Like Gman mentioned ConcurrentDictionary is the preferred way to do this, however if that is not available to a simple lock statement would suffice.

static Func<A, R> Memoize<A, R>(this Func<A, R> f)
{
    var d = new Dictionary<A, R>();
    return a=> 
    {
        R r;
        lock(d)
        {
            if (!d.TryGetValue(a, out r))
            {
                r = f(a);
                d.Add(a, r);
            }
        }
        return r;
    };
}

One potential issue using locks instead of ConcurrentDictionary is this method could introduce deadlocks in to your program.

  1. You have two memoized functions _memo1 = Func1.Memoize() and _memo2 = Func2.Memoize(), where _memo1 and _memo2 are instance variables.
  2. Thread1 calls _memo1, Func1 starts processing.
  3. Thread2 calls _memo2, inside Func2 there is a call to _memo1 and Thread2 blocks.
  4. Thread1's processing of Func1 gets to a call of _memo2 late in the function, Thread1 blocks.
  5. DEADLOCK!

So if at all possible, use ConcurrentDictionary, but if you can't and you use locks instead do not call other Memoized functions that are scoped outside of the function you are running in when inside Memoized functions or you open yourself up to the risk of deadlocks (if _memo1 and _memo2 been local variables instead of instance variables the deadlock would not have happened).

(Note, performance may be slightly improved by using ReaderWriterLock but you still will have the same deadlock issue.)

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
0

using System.Collections.Generic;

Dictionary<string, string> _description = new Dictionary<string, string>();
public float getDescription(string value)
{
     string lookup;
     if (_description.TryGetValue (id, out lookup)) {
        return lookup;
     }

     _description[id] = value;
     return lookup;
}
Svitlana
  • 2,938
  • 1
  • 29
  • 38