When you see somebody laying a claim like this, your first reaction should be - this is a hoax, there is no ultimate solution to double checked locking problem. And you will be correct.
Yet I do have an ultimate solution, although it only solves this problem for load/create on demand scenarios. It also involves some cheating. Let me explain myself.
Double Checked Locking - some background
Double checked locking is usually presented as a way to reduce contention around some global object used in a multi-threaded environment. It is pretty common in such systems to have global objects which are updated rarely but accessed very often. A solution to double checked locking problem would provide a way to first check whether the lock is necessary and only apply the lock if it is. When (and if) the lock is applied the usual pattern is used which also involves checking - hence the name 'double checked locking'
There are numerous, sometimes heated, discussions on the topic (as a starting point you can check here) but the bottom line is that all suggested generic solutions either have implicit locks or are not thread safe.
So, what about my claim?
Here is the scenario: Let us say you have a list of resources to be used by multiple threads. If resource creation is expensive you would want to ensure that every resource is created on demand the first time it is requested. So when a thread needs a resource it has to check first whether this resource was requested before and if it is, take the existing one rather than create another copy. The dictionary of the resources is that very object which would benefit from the double checked lock.
The thing is that in this particular case it is possible to safely implement the first check of the double checked lock schema outside of any locks. All you need to do is in addition to keeping a global dictionary make copies of the dictionary - one per processing thread and make every thread perform the check against its own dictionary. Any copy of the dictionary will be only accessed by the thread which owns it, so there is no locks necessary while checking this dictionary.
If the requested resource is located you can take it and use it. If it is not you have to check whether it was created by a different thread or you need to create it. To do that you have toВ acquire a lock on the global dictionary, check if the resource is already there, create and add it to the global dictionary if it is not and, finally create a new copy of the global dictionary to replace the copy in your worker thread, the usual stuff - all of this while holding the lock.
This second part is no different from the straightforward implementation of the global resource dictionary. The difference is that if it were the only one, every request for the resource would have to go through a global lock which in the systems with a big number of simultaneous requests can create a severe bottleneck. Adding extra layer with per thread dictionaries remedies this problem - every thread which already has a resource in its local dictionary will access it without any locking.
Let me be explicit about some assumptions here.
- First of all it is implied that all threads share a single copy of the resource. It means that either it has to be immutable or you will have to build some synchronization over access to the resource itself - which takes you back to the square one.
- Secondly the dictionary itself has to be immutable as well so that the same copy can be shared by multiple threads.
If it sounds very F# than that's because it is - this is exactly how NDjango rendering engine is implemented and it is written in F#
There is one more thing I have to admit to. Speaking theoretically my approach does not solve anything - it just pushes the same problem to the process responsible for thread allocation. Think about it - you have a list of available threads and when a new request comes in, a thread has to be allocated to it and removed from the list. It is the same problem. Luckily for us people creating these parts of our software/hardware (like ASP.NET core) are really smart and they solved this problem for us in the most efficient manner, so for all practical purposes it does solve the problem.