1

Is it possible to transfer data between threads like producer consumer using POSIX Message queue? i need to transfer and an array of double with 5000 elements each from producer thread to consumer thread for processing

is POSIX Message queue designed for such a purpose?

Altris
  • 39
  • 3

3 Answers3

2

POSIX message queues are absolutely the wrong tool for that.

All you actually need, is a buffer, a couple of counters or pointers, a mutex, and a couple of condition variables:

static pthread_mutex_t    buffer_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t     buffer_more = PTHREAD_COND_INITIALIZER;
static pthread_cond_t     buffer_room = PTHREAD_COND_INITIALIZER;
/* Pointer and counters are volatile, since different threads may change them
   whenever they hold the buffer_lock. */
static double * volatile  buffer_data = NULL;
static volatile size_t    buffer_size = 0;
static volatile size_t    buffer_next = 0;  /* First/next buffered value */
static volatile size_t    buffer_ends = 0;  /* First unused byte in buffer */

/* Optional flag to indicate no more data to be produced or consumed */
static volatile int       buffer_done = 0;


/* Helper function to repack the buffer; caller must hold buffer_lock. */
static inline void  buffer_repack_locked(void)
{
    if (buffer_ends > buffer_next) {
        if (buffer_next > 0) {
            memmove(buffer_data, buffer_data + buffer_next,
                    (buffer_ends - buffer_next) * sizeof buffer_data[0]);
            buffer_ends -= buffer_next;
            buffer_next = 0;
        }
    } else {
        buffer_next = 0;
        buffer_ends = 0;
    }
}

To grow the buffer (at any point), you use

static int  buffer_resize(size_t  new_size)
{
    pthread_mutex_lock(&buffer_lock);

    /* First, repack the buffer to start of the area. */
    buffer_repack_locked();

    /* Do not lose any data, however. */
    if (new_size < buffer_ends)
        new_size = buffer_ends;

    /* Reallocate. */
    void *new_data = realloc(buffer_data, new_size * sizeof buffer_data[0]);
    if (!new_data) {
        /* Not enough memory to reallocate; old data still exists. */
        pthread_mutex_unlock(&buffer_lock);
        return -1;
    }

    /* Success. */
    buffer_data = new_data;
    buffer_size = new_size;

    /* Wake up any waiters waiting on room in the buffer, just to be sure. */
    pthread_cond_broadcast(&buffer_room);

    pthread_mutex_unlock(&buffer_lock);
    return 0;
}

Producer or producers add a block of data to the buffer using

static void  buffer_add(const double *data, size_t count)
{
    pthread_mutex_lock(&buffer_lock);
    buffer_repack_locked();

    while (count > 0) {

        if (buffer_ends >= buffer_size) {
            /* Buffer is full. Wait for more room, repack, retry. */
            pthread_cond_wait(&buffer_room, &buffer_lock);
            buffer_repack_locked();
            continue;
        }

        /* How much can we add? */
        size_t  size = buffer_size - buffer_ends;
        if (size > count)
            size = count;

        memmove(buffer_data + buffer_ends, data, size * sizeof buffer_data[0]);
        buffer_ends += size;

        /* Wake up a consumer waiting on more data */
        pthread_cond_signal(&buffer_more);

        /* Update to reflect the data already added */
        data += size;
        count -= size;
    }

    /* All data added. */
    pthread_mutex_unlock(&buffer_lock);
}

Similarly, consumers get data from the buffer using

static size_t  buffer_get(double *data, size_t min_size, size_t max_size)
{
    size_t  size, have = 0;

    /* Make sure min and max size are in the right order. */
    if (max_size < min_size) {
        size = max_size;
        max_size = min_size;
        min_size = size;
    }

    pthread_mutex_lock(&buffer_lock);

    while (1) {

        /* No more data incoming? */
        if (buffer_done) {
            pthread_mutex_unlock(&buffer_lock);
            return have;
        }

        /* Buffer empty? */
        if (buffer_next >= buffer_ends) {
            pthread_cond_wait(&buffer_more, &buffer_lock);
            continue;
        }

        /* How much can we grab? */
        size = buffer_ends - buffer_next;
        if (have + size > max_size)
            size = max_size - have;

        memmove(data, buffer_data + buffer_next,
                size * sizeof buffer_data[0]);
        buffer_next += size;

        /* Wake up a waiter for empty room in the buffer. */
        pthread_cond_signal(&buffer_room);

        /* Enough data to return? */
        if (have >= min_size) {
            pthread_mutex_lock(&buffer_lock);
            return have;
        }
    }
}

While this does copy the data around quite a bit, it allows both producers and consumers to work on their own data in any size "chunks" they wish.


If your producers and consumers work on matrices, or other "packetized" data of some maximum size, it makes sense to use singly-linked lists of preallocated packets of data, and not a linear buffer:

struct data_packet {
    struct data_packet  *next;
    size_t               size; /* Maximum size of data */
    size_t               used; /* Or rows, cols if a matrix */
    double               data[];
};

struct data_queue {
    pthread_mutex_t      lock;
    pthread_cond_t       more;
    pthread_cond_t       room;
    struct data_packet  *queue;
    struct data_packet  *unused;
    unsigned long        produced;  /* Optional, just information */
    unsigned long        consumed;  /* Optional, just information */
    volatile int         done;      /* Set when no more to be produced */
};

static void free_data_packets(struct data_packet *root)
{
    while (root) {
        struct data_packet *curr = root;

        root = root->next;

        curr->next = NULL;
        curr->size = 0;
        free(curr);
    }
}

To initialize a data queue, we also need to generate some empty packets in it. This must be done before any threads start working with the queue:

/* Returns the count of data packets actually created,
   or 0 if an error occurs (with errno set).
*/
size_t   data_queue_init(struct data_queue *q,
                         const size_t  size,
                         const size_t  count)
{
    if (!q) {
        errno = EINVAL;
        return 0;
    }

    pthread_mutex_init(&(q->lock), NULL);
    pthread_cond_init(&(q->more), NULL);
    pthread_cond_init(&(q->room), NULL);
    q->queue = NULL;
    q->unused = NULL;
    q->produced = 0;
    q->consumed = 0;
    q->done = 0;

    /* Makes no sense to request no data packets. */
    if (count < 1) {
        errno = EINVAL;
        return 0;
    }

    /* Create a chain of empty packets of desired size. */
    struct data_packet *curr, *unused = NULL;
    size_t              have = 0;

    while (have < count) {
        curr = malloc( sizeof (struct data_packet)
                     + size * sizeof curr->data[0]);
        if (!curr)
             break;

        curr->next = unused;
        curr->size = size;
        curr->used = 0;
        unused = curr;
        have++;
    }

    if (!have) {
        errno = ENOMEM;
        return 0;
    }

    /* Attach chain to data queue; done. */
    q->unused = unused;
    return have;
}

Producers grab a free packet from the data queue:

struct data_packet *data_queue_get_unused(struct data_queue *q)
{
    /* Safety check. */
    if (!q) {
        errno = EINVAL;
        return NULL;
    }

    pthread_mutex_lock(&(q->lock));

    while (!q->done) {

        struct data_packet *curr = q->unused;

        /* No unused data packets free? */
        if (!curr) {
            pthread_cond_wait(&(q->room), &(q->lock));
            continue;
        }

        /* Detach and clear. */
        q->unused = curr->next;
        curr->next = NULL;
        curr->used = 0;

        /* Successful. */
        pthread_mutex_unlock(&(q->lock));
        return curr;
    }

    /* Done is set. */
    pthread_mutex_unlock(&(q->lock));
    errno = 0;
    return NULL;
}

The above may return NULL, when an error occurs (errno will be set to a nonzero error), or when the done flag is set (errno will be zero).

The producer must remember to set the used field to reflect the amount of data it produced in the packet. (It must not exceed size, though.)

The producer can work on the data packet as they wish; it is their "own", and no locking is needed.

When the producer has completed the packet, they append it to the data queue:

int data_queue_append(struct data_queue *q, struct data_packet *p)
{
    /* Safety check. */
    if (!q || !p) {
        errno = EINVAL;
        return -1;
    }
    p->next = NULL;

    pthread_mutex_lock(&(q->lock));

    /* Append to queue. */
    struct data_packet *prev = q->queue;
    if (!prev) {
        q->queue = p;
    } else {
        while (prev->next)
            prev = prev->next;
        prev->next = p;
    }

    q->produced++;

    /* Wake up a waiter for a new packet. */
    pthread_cond_signal(&(q->more));

    /* Done. */
    pthread_mutex_unlock(&(q->lock));
    return 0;
}

Similarly, a consumer grabs the next packet from the queue,

struct data_packet *data_queue_get(struct data_queue *q)
{
    /* Safety check. */
    if (!q) {
        errno = EINVAL;
        return NULL;
    }

    pthread_mutex_lock(&(q->lock));
    while (1) {

        struct data_packet *curr = q->queue;

        /* No data produced yet? */
        if (!curr) {
            /* If the done flag is set, we're done. */
            if (q->done) {
                pthread_mutex_unlock(&(q->lock));
                errno = 0;
                return NULL;
            }

            /* Wait for a signal on 'more'. */
            pthread_cond_wait(&(q->more), &(q->lock));
            continue;
        }

        /* Detach and done. */
        q->queue = curr->next;
        curr->next = NULL;

        q->consumed++;

        pthread_mutex_unlock(&(q->lock));
        return curr;
    }
}

and freely works on it. Note that the above does not examine the done flag unless the queue is empty.

When it is completed the work on the packet, it returns it to the unused queue:

int data_queue_append_unused(struct data_queue *q, struct data_packet *p)
{
    /* Safety check */
    if (!q || !p) {
        errno = EINVAL;
        return -1;
    }

    /* Clear it. */
    p->used = 0;

    pthread_mutex_lock(&(q->lock));

    /* Prepend to unused queue. */
    p->next = q->unused;
    q->unused = p;

    /* Signal a waiter that a new packet is available. */
    pthread_cond_signal(&(q->room));

    /* Done. */
    pthread_mutex_unlock(&(q->lock));
    return 0;
}

This approach allows one or more consumers and one or more producers work on their own packets on their own pace, without using any locks et cetera, and without copying the data itself around. However, the packet size and number of packets concurrently being worked on are limited.

The queue must be initialized with unused packet count at least the total number of producers and consumers; I prefer about twice that, to maximize throughput when the time taken by each varies a bit. The above, however, does allow removal of empty packets from the unused queue, and/or appending new empty packets to the unused queue, at any point in time. (When appending, remember to signal on the data queue room condition variable.)

Finally, note that the produced and consumed counts refer to the queue itself. If you want consumed to reflect the number of packets already consumed, you can move the q->consumed++ from data_queue_get() to data_queue_append_unused() instead.

None
  • 281
  • 1
  • 3
0

It will work, but be aware that the absolute maximum message size is 16 MB (HARD_MSGSIZEMAX) since Linux 3.5, and was 1 MB before that. The default message size limit is only 8 KB though, so you need to set it when you call mq_open() or your 5000 doubles won't fit in one message.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
0

A message queue is meant to transfer data between processes. Since threads are a part of the same process, there is no need to send data first to the kernel and then receive it back. In case of threads, all the global data is visible to all threads. Signalling mechanism like mutex and condition variables are required to synchronize the availability of data between threads.

kjohri
  • 1,166
  • 11
  • 10
  • So do you mean I should build a circular queue using array to transfer data from one thread to another My producer loop runs fast and consumer loop at a much slower speed, consumer waits untill a specific amount of data set has been reached and process it – Altris Jul 26 '20 at 11:09
  • Some kind of signalling mechanism should be used so that consumer comes to know that data is available. – kjohri Jul 26 '20 at 11:17
  • Cant POXIS MESSAGE QUEUE be used for producer consumer? – Altris Jul 26 '20 at 11:20
  • I think you mean POSIX message queues. You can use, but it is unnecessary because all the data is in a process. The problem here is synchronization and not communication. – kjohri Jul 26 '20 at 11:27