Writing Multi-threaded Applications with ASN1C

It’s fairly rare that we receive emails related to using ASN1C libraries in a multi-threaded application, but we know many applications use ASN1C-generated code and libraries in a multi-threaded environment. While the complexities of multi-threading are well without the scope of our blog, we did want to provide some guidance to those customers and evaluators who might want to write a multi-threaded ASN.1 encoding and decoding application. We’ll concentrate on C/C++ in this post, but the concepts are extensible to Java and C#.

The common ASN1C runtime library provides the OSCTXT data structure as a means of isolating and collecting all of the relevant data associated with a particular encoding or decoding process.

Our general rule of thumb is that one context or context-holding structure (like an OSRTMessageBuffer) must be allocated per thread to avoid the use of locks, mutexes, or semaphores. The sharing of a context implies that its data buffer may be written to from multiple threads, and you’ll need to serialize access in that case. Often this will result in the degradation of performance.

It’s relatively straightforward to create a multi-threaded application using our current code-generating options: adding –writer, –test, and –reader to the command-line will produce applications that can produce and consume messages.

In a simple reader application, a main function is generated as the entry point to the application. main can be renamed and modified fairly easily to work with Windows- or POSIX-compliant threads:

void *threaded_reader( void *thread_data )
{
   /* normal reader implementation follows here */
   
   return thread_data; /* a NULL return is normal, too */
}

The thread_data parameter is offered as a means of simplifying the interface for the thread libraries; instead of supplying multiple parameters, a data structure that encapsulates them can be passed in by casting to void *. (In object-oriented threading, this sort of handwaving is unnecessary because the object has its own data and implements a threading interface.)

In a quick case, I did something like this:

typedef struct ThreadData {
   int    argc;   /* how many arguments we have */
   char **argv;   /* the contents of the arguments */
   int    rc;     /* the return code after the thread finishes */
   const char *name;
                  /* the thread name */
   int    msg_cnt;
                  /* the message count */
} ThreadData;

This encapsulated all of the information that would normally be passed to the reader, and allowed a very easy setup for the following (elided) main function:

int main( int argc, char **argv )
{
   pthread_t thread1, thread2, thread3, thread4;

   ThreadData t_data1 = { argc, argv, 0, "no. 1", 0 },
              t_data2 = { argc, argv, 0, "no. 2", 0 },
              t_data3 = { argc, argv, 0, "no. 3", 0 },
              t_data4 = { argc, argv, 0, "no. 4", 0 };

   pthread_create( &thread1, NULL, &reader_thread, (void *)(&t_data1) );
   pthread_create( &thread2, NULL, &reader_thread, (void *)(&t_data2) );
   pthread_create( &thread3, NULL, &reader_thread, (void *)(&t_data3) );
   pthread_create( &thread4, NULL, &reader_thread, (void *)(&t_data4) );

   pthread_join( thread1, NULL );
   pthread_join( thread2, NULL );
   pthread_join( thread3, NULL );
   pthread_join( thread4, NULL );

   return 0;
}

The Windows-compliant version is similar, although the API calls are obviously different. This particular reader doesn’t do anything interesting, like process different files simultaneously, but it does pretty easily demonstrate how the data structures are limited on a per-thread basis. (Note that the OSCTXT structure is created and maintained within each of the reader_thread invocations. It would be equally reasonable to include a context in each ThreadData structure.)

More reading:

Happy threading!