Chapter 18 : Series On Design Patterns – Singleton Pattern

Key Words: Singleton pattern, logger

Topics at a glance:

  • Playing safe with singletons – how to incorporate singleton design pattern in multi-threaded environment
  • Let’s put singleton and concurrency into action into a logger application

Singleton pattern

We all know what a singleton pattern is. It is used for preventing creation of more than one instance of a class. For explaining singleton pattern, I have come up with a suitable example – A logger.

Logger

The logger class is a composite class; it has a queue, which is used for storing the received messages and getting messages one by one out of the queue and finally prints it.

There should be only one logger instance. The same instance will be shared between multiple threads and they all try to log messages through that shared instance. Logger class has one printer thread to pull the received messages one by one and prints it out to the console output. This printer thread’s print function is made as a friend function to this logger class for convenience reasons. i.e. to avoid the syntax horrors in passing a member function to a thread.

i.e. There are multiple producers here and one consumer.

Since logger instance is shared between multiple threads, a matter of shared ownership is there and should be managed through std::shared_ptr. Since we must make multiple producer threads, thread_guards should be in place to manage them.

And finally, one more puzzle to be solved. Since multiple threads will try to get the singleton object instance, there is chance for race condition. The only variable that we are checking in the get_logger_instance() static member function is a static boolean flag ‘instance_created’. So, to avoid a likely race condition, we have to protect it with a mutex, managed through a unique_lock. Note: A race condition here may result in failure of singleton design and may create multiple logger instances.

So that’s it. For the singleton logger class, we have to use almost all C++ features that we have discussed so far:

  • Static members
  • Static member functions
  • Private constructor
  • Mutex locks
  • Unique locks
  • Shared pointers
  • Friend function
  • “Has a” relationship
  • thread guards

All these will be used in our logger class. Let us see this in action now.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#include <iostream>
#include <thread>
#include <condition_variable>
#include <string>
#include <memory>
#include <atomic>

using namespace std;

template<typename T>
class Queue
{
public:
    enum class error_codes{bad_size = -1};
private:
    T *memory;
    int size_;
    int head;
    int tail;
    bool is_full;
    bool is_empty;
    std::mutex mutex_;
    std::condition_variable cond_var;
    bool data_ready;
public:
    explicit Queue(const int size = 0) : 
        size_{size}, data_ready{false}, is_full{false}, is_empty{true}
    {
        if(size_ <= 0)
        {
            cout << "Bad size specified" << endl;
            throw error_codes::bad_size;
        }
        
        memory = new T[size_];
        
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is created " << endl;
    }
    
    ~Queue()
    {
        delete []memory;
        head = 0;
        tail = 0;
        cout << "Queue with size : " << size_ << " is destroyed" << endl;
        size_ = 0;
    }
    
    // delete copy/move operations 
    Queue(const Queue& other_queue) =delete;
    Queue& operator=(const Queue& other_queue) =delete;
    Queue(Queue&& other_queue) =delete;
    Queue& operator=(Queue&& other_queue) =delete;
    
    
    bool insert(const T data)
    {
        bool ret = true;
        std::unique_lock<mutex> lck_(mutex_);
        
        if( is_full == false )
        {
            memory[tail] = data;
            tail = (tail + 1) % size_;
            if(tail == head)
            {
                is_full = true;
            }
            is_empty = false;
        }
        else
        {
            // queue is full
            cout << "\tQueue is full : " << data  << endl;
            ret = false;
        }
        
        if(ret == true)
        {
            data_ready = true;
            cond_var.notify_one(); // unlocks the lock within the function 
        }
        
        //lck_.unlock(); // actually unnecessary to do this for unique_lock 
        
        return ret;
    }
    
    bool get(T * data)
    {
        bool ret = true;
        
        unique_lock<mutex> lck(mutex_);
        
        // predicate 'check for data_ready' is VERY IMPORTANT here 
        cond_var.wait(lck, [this]{return this->data_ready;} );
        
        if(is_empty == false)
        {
            *data = memory[head];
            head = (head + 1) % size_;
            is_full = false;
            if(head == tail)
            {
                is_empty = true;
            }
        }
        else
        {
            // queue empty
            cout << "\tQueue is empty" << endl;
            ret = false;
        }
        
        return ret;
    }
    
    int size() const
    {
        return size_;
    }
    
};

class logger;
void print(const logger* this_logger);

class logger
{
private:
    static shared_ptr<logger> instance;
    static std::mutex mutex_;
    static bool instance_created;
    
    int size_;
    Queue<std::string> *queue;
    std::thread printer_thread;
    
    // private constructor
    logger() : 
        size_{100},  
        queue( new Queue<std::string>(size_) ),
        printer_thread(print, this)
    {
        cout << "Logger instance created" << endl;
    }
    
public:
    ~logger()
    {
        end_session();
        printer_thread.join();
        delete queue;
        cout << "Logger destroyed" << endl;
    }
    
    static shared_ptr<logger> get_logger_instance()
    {
        unique_lock<std::mutex> lck(mutex_);
        
        if(instance_created == false)
        {
            // creates a logger instance
            instance.reset(new logger());
            instance_created = true;
            lck.unlock();
        }
        else
        {
            cout << "Logger instance already created. Returning instance" << endl;
        }
        
        return instance;
    }
    
    bool log_info(const string& info)
    {
        string temp("INFO : ");
        temp += info;
        return queue->insert(temp);
    }
    
    bool log_error(const string& error)
    {
        string temp("ERROR : ");
        temp += error;
        return queue->insert(temp);
    }
    
    bool end_session()
    {
        string temp("END_LOG");
        return queue->insert(temp);
    }
    
    friend void print(const logger *this_logger);
    
};

std::mutex logger::mutex_;
bool logger::instance_created = false;
shared_ptr<logger> logger::instance(nullptr);

void print(const logger *this_logger)
{
    string message;
    cout << "printer thread spawned" << endl;
    while(true)
    {
        this_logger->queue->get(&message);
        cout << message << endl;
        
        if(0 == message.compare("END_LOG"))
        {
            cout << "Printer thread execution complete" << endl;
            break; 
        }
    }
    
    return;
}

void log_hello(void)
{
    shared_ptr<logger> sh_logger = logger::get_logger_instance();
    logger *logger_instance = sh_logger.get();
    
    for(int count = 0; count < 5; ++count)
    {
        logger_instance->log_info(" Hello ");
    }
    
    return;
}

void log_world(void)
{
    shared_ptr<logger> sh_logger = logger::get_logger_instance();
    logger *logger_instance = sh_logger.get();
    
    for(int count = 0; count < 5; ++count)
    {
        logger_instance->log_info(" World ");
    }
    
    return;
}

class thread_guard
{
private:
    std::thread this_thread;
public:
    explicit thread_guard(void(*th_function)(void)):
        this_thread(th_function)
    {
        // do nothing
    }
    ~thread_guard()
    {
        this_thread.join();
        cout << "Thread_Guard : Thread joined" << endl;
    }
    // delete copy/move operations
    thread_guard(const thread_guard&) =delete;
    thread_guard(thread_guard&&) =delete;
    thread_guard& operator=(const thread_guard&) =delete;
    thread_guard& operator=(thread_guard&&) =delete;
};

int main()
{    
    thread_guard guard_hello_thread(log_hello);
    thread_guard guard_world_thread(log_world);
    
    cout << "Main ends" << endl;
    
    return 0;
}

Want to see the result?

Queue with size : 100 is created 
Main ends
Logger instance created 
printer thread spawned
INFO :  Hello
Logger instance already created. Returning instance
INFO :  Hello
Thread_Guard : Thread joined
INFO :  Hello
Thread_Guard : Thread joined
INFO :  Hello
INFO :  Hello
INFO :  World
INFO :  World
INFO :  World
INFO :  World
INFO :  World
END_LOG
Printer thread execution complete
Queue with size : 100 is destroyed
Logger destroyed

Enjoyed the chapter? Let me know in the comments below. Thanks 🙂

0

Leave a Reply