Search code examples
cpthreadsfreesoftware-design

How are types like pthread_mutex_t implemented in C?


I want to bring some of those properties to types created by me, if possible.

More specifically I want to know more about these:

  1. How is it possible the code doesn't compile if I try to manually assign any value to a pthread_mutex_t object?
  2. If it is implemented as a struct, why can't I access any field?
  3. The most important one, how is it possible that I can safely call pthread_mutex_destroy on an uninitialized object, and the call will simply return error without risking breaking anything? If I did the same with a data structure of mine, I would risk trying to free some random pointers or in general closing resources that were never opened, possibly breaking the code.

The reason why I need the first two is to achieve better encapsulation, the third one to be able to write cleanup code easier in case of failure. Specifically, if a function fails in the middle of allocating multiple resources possibly of different types, one way I thought of freeing them before returning is not to keep track of where the program failed and what resources were allocated at the time of failure, but rather use free/destroy functions that don't do anything when the resource you call them on was not allocated/initialized, and during cleanup call these functions on every object declared (that might or might not have not been initialized).

pthread_mutex_destroy seems to fall into this category, but also the standard free, if you assign all pointers to be malloced a NULL at the top of the code. Is this a good way of handling this?


Solution

  • How is it possible the code doesn't compile if I try to manually assign any value to a pthread_mutex_t object?

    Simply put, because the compiler does not know the real type behind pthread_mutex_t. It is not exported in the library headers (pthread.h), so it cannot "guess" what to do when assigning something else to it.

    If it is implemented as a struct, why can't I access any field?

    Because the underlying struct is hidden from you, and you only see the typedef exported by library headers. The pthread.h header will just contain:

    typedef struct some_internal_name pthread_mutex_t;
    

    The important thing is that the definition of struct some_internal_name is available to the internal parts of the library that need to access its fields. Library users do not (and should not) do it, so they don't need the struct definition and only see the typedef.

    This is standard practice and pretty common in C: pthread_t from pthread.h, FILE from stdio.h, CURL from curl/curl.h (libcurl). Other libraries just use void * instead of their internal type, for example dlopen() from dlfcn.h (libdl) returns void * but that is actually a pointer to an internal struct.

    how is it possible that I can safely call pthread_mutex_destroy on an uninitialized object, and the call will simply return error without risking breaking anything?

    It is not safe to do so. See man pthread_mutex_destroy.3p:

    The behavior is undefined if the value specified by the mutex argument to pthread_mutex_destroy() does not refer to an initialized mutex.

    If you have code that does this and somehow still works, it's likely only a coincidence. In fact, an uninitialized variable of any kind could hold any kind of invalid data, so it's almost always unsafe to use it. In the majority of the cases, it is also undefined behavior to do so according to the C standard. The only exceptions are objects with static storage duration that have a well-defined behavior as they are zeroed-out by definition.

    one way I thought of freeing them before returning is not to keep track of where the program failed and what resources were allocated at the time of failure

    Unfortunately you cannot just always free all objects (even uninitialized ones). There is a common pattern in C using goto labels that can help you though:

    void example(void) {
        pthread_mutex_t mutex;
        FILE *fp;
        char *mem;
    
        if (pthread_mutex_init(&mutex, NULL) != 0)
            return;
    
        if ((fp = fopen("example.txt", "r")) == NULL)
            goto out_destroy_mutex;
    
        if ((mem = malloc(0x100)) == NULL)
            goto out_close_file;
    
        /* do whatever you need here */
    
        free(mem);
    
    out_close_file:
        fclose(fp);
    out_destroy_mutex:
        pthread_mutex_destroy(&mutex);
        return;
    }
    

    If you can, you should use PTHREAD_MUTEX_INITIALIZER so that you can then safely destroy any mutex you define and initialize this way without a problem:

    void example(void) {
        pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
        pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
        pthread_mutex_t m3 = PTHREAD_MUTEX_INITIALIZER;
    
        /* ... */
    
        pthread_mutex_destroy(&m1);
        pthread_mutex_destroy(&m2);
        pthread_mutex_destroy(&m3);
    }
    

    Honestly though, you do not really need to destroy mutexes. I don't know your exact use case, but in the above examples just returning from the function without explicitly calling pthread_mutex_destroy() is also fine. If you are worried about memory usage "destroying" a mutex does not actually free any memory. It just modifies the data in the underlying structure.