ajax_velu
ajax_velu

Reputation: 296

Is the following C function thread-safe?

I saw a blog stating the below code is thread safe , but the condition count not being inside the mutex would cause a data corruption; in case two threads check the count at the same time but before either acquires a mutex and one acquire the mutex after contending. When one thread completes , the other would very blindly add one more value to the array.

char arr[10];
int count=0;

int func(char ctr)
{
    int i=0;
    if(count >= sizeof(arr))
    {
        printf("\n No storage\n");
        return -1;
    }

     /* lock the mutex here*/

    arr[count] = ctr;
    count++;

    /* unlock the mutex here*/

    return count;
}

Would I be right if I made the following changes? Or is there a better/efficient way to do it

   int func(char ctr)
    {
    int i=0;

    /* lock the mutex here*/

    if(count >= sizeof(arr))
    {
        printf("\n No storage\n");

        /* unlock the mutex here*/

        return -1;
    }


    arr[count] = ctr;
    count++;

    /* unlock the mutex here*/

    return count;
}`

Upvotes: 3

Views: 173

Answers (3)

david.pfx
david.pfx

Reputation: 10863

Nothing wrong with the previous answers, but there is a better way. A mutex is not needed.

int func(char ctr) {
    int c = interlocked_increment(&count);
    if (c >= sizeof(arr)) {
        printf("\n No storage\n");
        interlocked_decrement(&count);
        return -1;
    }
    arr[c-1] = ctr;
    return c;
}

This depends on the availability of interlocked increment and decrement functions, which have to be provided by your operating system or third party library.


Every value of c that is within range is guaranteed to be one not seen by any other thread, and is therefore a valid slot in arr, and no thread will miss a slot if there is one available. The order of storing the value is indeterminate, but that is true of most of the other solutions too. The maximum value reached by count if many threads are competing for storage is also indeterminate, and if that is an issue a different approach might be needed. The behaviour if count is decremented is another unknown. This stuff is hard, and it's always possible to add additional constraints to make it harder.


Just to point out that there is another possible implementation based on a CSET (Check and Set) function. This is a function that checks whether some location is equal to a value and if so sets it to another value atomically, returning true if so. It avoids some of the criticism leveled at the above function.

int func(char ctr) {
    for (;;) {
      int c = count;
      if (c >= sizeof(arr)) {
        printf("\n No storage\n");
        return -1;
      }
      if (CSET(&count, c, c+1)) {
        arr[c] = ctr;
        return c;
      }
  }
}

The C++ standard atomic operations library contains a set of atomic_compare_exchange functions which should serve the purpose, if available.

Upvotes: 0

user3386109
user3386109

Reputation: 34839

First, you are correct that the code from the blog has the potential to write beyond the end of the array. The limit checking only works if it's done after the mutex has been acquired.

Here's how I would write the function:

bool func(char ctr)
{
    bool result;

    /* lock the mutex here */

    if (count >= sizeof(arr))
    {
        result = FAILURE;
    }
    else
    {
        arr[count] = ctr;
        count++;
        result = SUCCESS;
    }

    /* unlock the mutex here */

    if ( result == FAILURE )
        printf("\n No storage\n");

    return result;
}

The features of this code worth noting

  • The mutex lock and unlock appear only once in the function, and there are no return statements in the critical section. This makes it clear that the mutex will always be unlocked.
  • The printf is outside of the critical section. printf is relatively slow, and any function that uses a mutex should hold the mutex for as little time as possible.
  • IMO the function shouldn't return a count, but rather should only return a bool indicating success or failure. Any code that needs to know how many entries are in the array should lock the mutex and examine the count directly.

Upvotes: 2

Rafael Almeida
Rafael Almeida

Reputation: 2397

You are correct. By doing the check outside of the critical section you are opening the doors for a possible buffer overrun. However, note that the returned count may not be the same index used to store ctr. That's an issue even in the corrected code.

In order to remedy that you could rewrite it like this:

int func(char ctr)
{
    /* lock the mutex here*/

    if(count >= sizeof(arr)) {
        printf("\n No storage\n");

        /* unlock the mutex here*/

        return -1;
    }


    arr[count] = ctr;
    int c = count++;

    /* unlock the mutex here*/

    return c;
}

It's worth noting that, if that's the only function changing "count", then no two threads would be able to change the same memory position in arr and this would actually be safe:

int func(char ctr)
{
    /* lock the mutex here*/

    if(count >= sizeof(arr)) {
        printf("\n No storage\n");

        /* unlock the mutex here*/

        return -1;
    }

    int c = count++;

    /* unlock the mutex here*/

    arr[c] = ctr;

    return c;
}

If that's the pattern, maybe you can refactor that code into two functions, like so:

int get_sequence(void)
{
    /* lock the mutex here*/

    int c = count++;

    /* unlock the mutex here*/

    return c;
}

int func(char ctr)
{
    int c = get_sequence();
    if(c >= sizeof(arr)) {
        printf("\n No storage\n");
        return -1;
    }

    arr[c] = ctr;

    return c;
}

Note that will only work as long as get_sequence is really the only function changing count variable.

Upvotes: 4

Related Questions