Thread-safety of a function refers to the fact whether a function can be safely called from multiple threads simultaneously. Safety here means that even if multiple threads are executing the function simultaneously the data integrity is intact and is not intermingled.

To ensure thread-safety we can use tools like pthread-mutex, condition variables etc. Let’s take a look at the below program:


#include
#include
#include
#include 

/* Prototypes */
void * thread_func1(void *);
void * thread_func2(void *);
void manipVal(int);

int var = 0;
pthread_mutex_t m;

void * thread_func1(void *arg)
{
char buff[1024];

manipVal(10);

printf("%d\n", var);

return NULL;
}

void * thread_func2(void *arg)
{
char buff[20124];
manipVal(20);

printf("%d\n", var);

return NULL;
}

void manipVal(int val)
{
pthread_mutex_lock(&m);
var = val;
pthread_mutex_unlock(&m);
}

int main(void)
{
pthread_t thread1, thread2;
int ret;

ret = pthread_create(&thread1, NULL, thread_func1, NULL);
if(ret < 0)
printf("Thread creation failed\n");

ret = pthread_create(&thread2, NULL, thread_func2, NULL);
if (ret < 0)
printf("Failed to create thread\n");

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

return 0;
}

The code can be downloaded from here pthread_thread_safe.c.

In thread-safety the focus is on the integrity of the data and proper protection is used for to that extent.

If you have very large function and if you take a mutex at the very beginning of the function and unlock the mutex at the end of the function then it will be a thread-safe function by definition but it will not be efficient though. Because no two thread can enter the function at the same time, one has to wait for the other to finish till the end. That’s why programmer’s focus is always to take the mutex lock at a much finer granularity. The best option is to attach the mutex to the resource (maybe data or other resource) being shred.

Re-entrancy refers to functions which do not use locking mechanism at all, in other words do not rely on thread synchronization. Also, re-entrant functions do not use any static or global data at all. A re-entrant function will typically be passed a pointer to a data structure allocated by the calling thread or function. As the function gets thread specific data, there is no chances of inter-mingling of data from different threads at all, as a result the function can be called from various threads at the same time which in turn enhances the performance. Let’s make out thread-safe function manipVal() a re-entrant function as below:


#include
#include
#include
#include 

/* Prototypes */
void * thread_func1(void *);
void * thread_func2(void *);
void manipVal(void *);

int var = 0;
pthread_mutex_t m;

typedef struct thread_data {
int a;
int b;
int res;
}thread_data_t;

void * thread_func1(void *arg)
{
char buff[1024];
thread_data_t * data = malloc(sizeof(thread_data_t));

data->a = 20;
data->b = 30;

manipVal(data);
sprintf(buff, "%d\n", data->res);
write(2, buff, 4);

free(data);

return NULL;
}

void * thread_func2(void *arg)
{
char buff[1024];
thread_data_t * data = malloc(sizeof(thread_data_t));
data->a = 50;
data->b = 10;

manipVal(data);

sprintf(buff, "%d\n", data->res);
write(2, buff, 4);

free(data);

return NULL;
}

void manipVal(void *data)
{
thread_data_t *temp = (thread_data_t *)data;
temp->res = temp->a * temp->b;
}

int main(void)
{
pthread_t thread1, thread2;
int ret;

ret = pthread_create(&thread1, NULL, thread_func1, NULL);
if(ret < 0)
printf("Thread creation failed\n");

ret = pthread_create(&thread2, NULL, thread_func2, NULL);
if (ret < 0)
printf("Failed to create thread\n");

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

return 0;
}

The source code can be found here as well reentrant.c.

Here both the threads are using their own allocated memory block “data” to the function manipVal() and the function strinctly operates on the thread specific data. Even if the function is executed on two different processors from two different threads at the same time there is no chance of the function corrupting the data of one thread with the other thread data as both the thread uses their own separate copies. This time the manipVal() function doesn’t use the synchronization mechanism as well as doesn’t use any static data like the “var” in the previous case.

So basically re-entrant functions prefers to use 1. thread specific data over synchronization for better performance 2. doesn’t use any static data so that there is no contention in the first place.

For further details please refer to the book Programming with POSIX threads section 1.2.5 which is the source of this blog as well.

Any further questions/queries please post your comments below.

Leave a Reply