The singleton pattern

The singleton pattern is used to create a single instance of an object and reuse it from anywhere. A singleton object is typically constructed when the application starts, or it can be created lazily on the first use of the object. An interesting requirement for the singleton pattern arises for multithreaded applications because the instantiation of the singleton object must happen only once. It can be a challenge if the object creation function is called lazily from many threads.

Suppose that we want to create a singleton called AppKey that is used for encryption in the application:

# AppKey contains an app id and encryption key
struct AppKey
appid::String
value::UInt128
end

Initially, we may be tempted to use a global variable. Given that we have learned about the performance impact of global variables, we can apply the global constant pattern that we learned in Chapter 6, Performance Patterns. Essentially, a Ref object is created as a placeholder, as follows:

# placeholder for AppKey object. 
const appkey = Ref{AppKey}()

The appkey global constant is first created without being assigned with any value, but then it can be updated when the singleton is instantiated. The construction of singleton can be done as follows:

function construct()
global appkey
if !isassigned(appkey)
ak = AppKey("myapp", rand(UInt128))
println("constructing $ak")
appkey[] = ak
end
return nothing
end

This code works fine as long as there is a single thread. If we test it with multiple threads, then the isassigned check is problematic. For example, two threads might check whether the key is assigned at the same time, and both threads might think that the singleton object needs to be instantiated. In this case, we end up constructing the singleton twice.

The testing code is shown as follows:

function test_multithreading()
println("Number of threads: ", Threads.nthreads())
global appkey
Threads.@threads for i in 1:8
construct()
end
end

We can demonstrate the problem below. Let's start the Julia REPL with four threads:

Then, we can run the testing code:

As you can see, the singleton being constructed twice here.

So how do we solve this problem? We can use a lock to synchronize the singleton construction logic. Let's first create another global constant to hold the lock:

const appkey_lock = Ref(ReentrantLock())

To use the lock, we can modify the construct function as follows:

We must first acquire the lock before checking whether appkey[] is already assigned. When we are done constructing the singleton (or skipping it, if it has already been created), we release the lock. Note that we have wrapped the critical section of the code in a try block, and we placed the unlock function in the finally block. This is done to ensure that the lock is released regardless of whether the construction of the singleton is successful or not.

Our new test shows that the singleton is constructed only once:

The singleton pattern is useful when we need to hold on to a single object. Practical use cases include database connections or other references to external resources. Next, we will take a look at the builder pattern.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.70.203