bottom prev next

Singleton

Singleton is a restrictive pattern that may be used to limit the number of instances of a given data type to one.

At run time a typical C program has access to two basic types of memory: external, backed by shared memory segments or memory-mapped files for example, and internal, backed by various segments: text, initialized and uninitialized data, heap and stack, of which we will be interested in heap and initialized data segment or simply data segment. For brevity sake, let us agree to call the singletons created:

  • in the external memory - External Singletons or ESs

  • on the heap (at run time) - Heap Singletons or HSs

  • in the data segment (at compile time) - Data Segment Singletons or DSSs

We will look at HSs together while DSSs and ESs will be left for your personal investigation.

The Singleton pattern consequences:

  • All the threads of, potentially, all the processes on a host have to acquire the same lock when attaching to the single instance of a given data type


Sample Problem

Implement the Heap Singleton pattern for bio_t from Factory chapter.


Generic Solution Description

1) Add the self-identification functionality to the public interface of a given data type.

2) Make the Construct()/Destruct() functions of a given data type private.

3) Remove the generic data type argument from the prototypes of this type's public functions.

4) Add the instance-restricting code to the New()/Delete() pair of functions.


Sample Solution

Step 1

bio_t's self-identification interface, bioName(), should be already present in the files borrowed from Factory chapter (add it if it is not, consult Factory chapter for details).


Step 2

Remove Construct()/Destruct() prototypes and bio_t arguments from the prototypes of public functions from libbio.h:

#include <sys/types.h> typedef struct bio { int ( *open )( struct bio*, const char* ); ssize_t ( *read )( struct bio*, void*, size_t ); ssize_t ( *write )( struct bio*, void*, size_t ); int ( *close )( struct bio* ); const char* ( *name )(); size_t ( *sizeOf )(); void ( *destruct )( struct bio* ); } bio_t; extern bio_t* bioNew( const char* ); extern void bioDelete(); extern size_t bioSizeOf( const char* ); extern int bioOpen( const char* ); extern ssize_t bioRead( void*, size_t ); extern ssize_t bioWrite( void*, size_t ); extern int bioClose(); extern const char* bioName(); extern size_t biosizeof();


Step 3

In libbio.c:

- make bioConstruct()/bioDestruct() private by designating their linkage specification as static

- add a biosingleton_t structure that will maintain the state of bio_t's sole instance by recording its address, number of users and a lock

- initialize a variable of type biosingleton_t to "no users" state

#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <dlfcn.h> #include <errno.h> #include "libbio.h" static bio_t* bioConstruct( void*, const char* ); static void bioDestruct( bio_t* ); typedef struct { bio_t* bios; size_t biosusers; pthread_mutex_t bioslock; } biosingleton_t; static biosingleton_t BIOS = { ( bio_t* )NULL, 0, PTHREAD_MUTEX_INITIALIZER };


Step 4

We will use the following single instance-attaching algorithm:

lock if ( first user ) { create the only bio_t instance record it's address } else if ( requested and singleton's bio_t names do not match ) { indicate an error } increment the number of users unlock

Implement the above algorithm in libbio.c in bioNew():

extern bio_t* bioNew( const char* bionm ) { bio_t* rv = ( bio_t* )NULL; pthread_mutex_lock( &BIOS.bioslock ); if ( BIOS.biosusers == 0 ) { size_t n = bioSizeOf( bionm ); void* mem = calloc( 1, n ); BIOS.bios = bioConstruct( mem, bionm ); } else if ( strcmp( bionm, bioName() ) ) { printf( "%s.%d: bioNew(): %s != %s\n", __FILE__, __LINE__, bionm, bioName() ); goto unlock; } BIOS.biosusers++; rv = BIOS.bios; printf( "%s.%d: bioNew(): %zu BIO Singleton user(s) attached.\n", __FILE__, __LINE__, BIOS.biosusers ); unlock: pthread_mutex_unlock( &BIOS.bioslock ); return rv; }


Step 5

We will use the following single instance-detaching algorithm:

lock if ( no users ) { ignore } if ( number of users is one ) { destruct singleton set its address to NULL } decrement the number of users unlock

Implement the above algorithm in libbio.c in bioDelete():

extern void bioDelete() { pthread_mutex_lock( &BIOS.bioslock ); if ( BIOS.biosusers == 0 ) { goto unlock; } if ( BIOS.biosusers == 1 ) { bioDestruct( BIOS.bios ); free( BIOS.bios ); BIOS.bios = ( bio_t* )NULL; printf( "%s.%d: bioDelete(): " "last BIO Singleton user detaching: " "deleting single instance.\n", __FILE__, __LINE__ ); } BIOS.biosusers--; printf( "%s.%d: bioDelete(): %zu BIO Singleton user(s) attached.\n", __FILE__, __LINE__, BIOS.biosusers ); unlock: pthread_mutex_unlock( &BIOS.bioslock ); }


Step 6

Make the remaining public I/O functions use BIOS variable in libbio.c::

extern int bioOpen( const char* adrs ) { int rv; rv = BIOS.bios->open( BIOS.bios, adrs ); return rv; } extern ssize_t bioRead( void* b, size_t bsz ) { ssize_t rv; rv = BIOS.bios->read( BIOS.bios, b, bsz ); return rv; } extern ssize_t bioWrite( void* b, size_t bsz ) { ssize_t rv; rv = BIOS.bios->write( BIOS.bios, b, bsz ); return rv; } extern int bioClose() { int rv; rv = BIOS.bios->close( BIOS.bios ); return rv; } extern const char* bioName() { const char* rv; rv = BIOS.bios->name(); return rv; } extern size_t biosizeof() { size_t rv; rv = BIOS.bios->sizeOf(); return rv; }

bioSizeOf(), libbiofile.c, libbiotcpipv4.c and libbiotcpipv6.c, not shown here, are borrowed verbatim from Factory chapter.


Step 7

Other than their linkage specification bioConstruct()/bioDestruct() remain the same. We have moved them to the bottom of libbio.c:

static bio_t* bioConstruct( void* mem, const char* bionm ) { bio_t* bio; bio_t* ( *biocnstrct )( void* ); char fnm[ 1024 + 1 ] = { 0 }; size_t fnmsz = sizeof( fnm ); snprintf( fnm, fnmsz, "bioConstruct%s", bionm ); biocnstrct = ( bio_t* ( * )( void* ) )dlsym( RTLD_DEFAULT, fnm ); bio = biocnstrct( mem ); return bio; } static void bioDestruct( bio_t* bio ) { bio->destruct( bio ); }


Step 8

Rebuild the I/O Factory adding the corresponding threads library to its link lines.

SLBTF:

gcc -D_GNU_SOURCE -g -c -fPIC -I . libbio.c gcc -g -c -fPIC -I . libbiofile.c gcc -g -c -fPIC -I . libbiotcpipv4.c gcc -g -c -fPIC -I . libbiotcpipv6.c gcc -g -shared -o libbio.so \ libbio.o \ libbiofile.o \ libbiotcpipv4.o \ libbiotcpipv6.o -ldl -lpthread


MLBTF:

gcc -D_GNU_SOURCE -g -c -fPIC -I . libbio.c gcc -g -shared -o libbio.so libbio.o -ldl -lpthread gcc -g -c -fPIC -I . libbiofile.c gcc -g -shared -o libbiofile.so libbiofile.o gcc -g -c -fPIC -I . libbiotcpipv4.c gcc -g -shared -o libbiotcpipv4.so libbiotcpipv4.o gcc -g -c -fPIC -I . libbiotcpipv6.c gcc -g -shared -o libbiotcpipv6.so libbiotcpipv6.o


MLRTF:

Add the -DIO_MLRTF option to MLBTF's libbio.c build line above:

gcc -D_GNU_SOURCE -DIO_MLRTF -g -c -fPIC -I . libbio.c


Step 9

Adjust the sample program to exercise the Heap Singleton bio_t pattern.

bio.c:

#include <stdio.h> #include <stdlib.h> #include "libbio.h" extern int main( int argc, char* argv[] ) { size_t i; const char* bionm; const char* adrs; bio_t* bio; char b[ 59 ] = { 0 }; size_t bsz = sizeof( b ) - 1; bio_t* vars[ 8 ] = { 0 }; size_t nvars = sizeof( vars ) / sizeof( vars[ 0 ] ); if ( argc != 3 ) { return -1; } bionm = argv[ 1 ]; adrs = argv[ 2 ]; bio = bioNew( bionm ); printf( "I/O Singleton address: %p\n", ( void* )bio ); for ( i = 0; i < nvars; i++ ) { vars[ i ] = bioNew( bionm ); printf( "&vars[ %zu ] = %p\n", i, ( void* )vars[ i ] ); } bioNew( "elif" ); bioOpen( adrs ); bioRead( b, bsz ); printf( "First %zu bytes:\n%s\n", bsz, b ); for ( i = 0; i < nvars; i++ ) { bioDelete(); } bioDelete(); return 0; }


Step 10

Build the sample Head Singleton application.

SLBTF and MLRTF:

gcc -g -c -I . bio.c gcc -g -L . -o bio bio.o -lbio

SLBTF and MLRTF:

gcc -g -c -I . bio.c gcc -g -L . -o bio bio.o -lbio -lfile -ltcpipv4 -ltcpipv6


Step 11

Run the sample program with various inputs:

./bio file bio.c libbio.c.72: bioNew(): 1 BIO Singleton user(s) attached. I/O Singleton address: 0x90e86b0 libbio.c.72: bioNew(): 2 BIO Singleton user(s) attached. &vars[ 0 ] = 0x90e86b0 libbio.c.72: bioNew(): 3 BIO Singleton user(s) attached. &vars[ 1 ] = 0x90e86b0 libbio.c.72: bioNew(): 4 BIO Singleton user(s) attached. &vars[ 2 ] = 0x90e86b0 libbio.c.72: bioNew(): 5 BIO Singleton user(s) attached. &vars[ 3 ] = 0x90e86b0 libbio.c.72: bioNew(): 6 BIO Singleton user(s) attached. &vars[ 4 ] = 0x90e86b0 libbio.c.72: bioNew(): 7 BIO Singleton user(s) attached. &vars[ 5 ] = 0x90e86b0 libbio.c.72: bioNew(): 8 BIO Singleton user(s) attached. &vars[ 6 ] = 0x90e86b0 libbio.c.72: bioNew(): 9 BIO Singleton user(s) attached. &vars[ 7 ] = 0x90e86b0 libbio.c.64: bioNew(): elif != file First 58 bytes: #include <stdio.h> #include <stdlib.h> #include "libbio.h" libbio.c.104: bioDelete(): 8 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 7 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 6 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 5 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 4 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 3 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 2 BIO Singleton user(s) attached. libbio.c.104: bioDelete(): 1 BIO Singleton user(s) attached. libbio.c.99: bioDelete(): last BIO Singleton user detaching: deleting single instance. libbio.c.104: bioDelete(): 0 BIO Singleton user(s) attached.


Exercises

1) Implement a Data Segment bio_t Singleton.

2) Implement an External bio_t Singleton.

3) Should a data type implemented as Singleton have a cloning interface?


Files

libbio.h libbio.c libbiofile.c libbiotcpipv4.c libbiotcpipv6.c mklib.sh

bio.c mkapp.sh

\(\blacksquare\)

top prev next