Overview

ServerKit provides basic queues that are intended for use among threads. It's basically a simple linked list, but with POSIX threads support added in the form of some synchronization primitives.

If you need a linked list based queue but for use in a single execution context, do not use these. They will add more overhead than is necessary because they perform synchronization.

The up side to this, is if you do have threads that need to queue data between one another, you can use these functions without worrying about synchronization.

When you want to send data through a queue, you use the server_queue_push() function, you provide it the queue you want to use and a void * pointing at the data you wish to queue.

You must ensure that the memory occupied by what you are queueing persists, it can't be on the local stack, and generally you will want to use something with reference counting and also some synchronization primitives so it can be placed on multiple queues without making new copies of itself.

The queue implementation doesnt touch the object you pass through it. It simply moves the pointer through, and it is up to the receiver to make use of it.

If you need to queue something simple like a 32 bit integer (fd?) you can generally cast it to a void * and push it through without having to worry about allocating additional memory. But when your application gets more complex you will need more than the 32 or 64 bits the void * provides (depending on the architecture), you may want to look at the ServerKit heaps if it fits your needs.

On the recipient end of the queue, that is, any thread that wants to wait for something to come through the queue, you call server_queue_pull(). This function takes 3 arguments, the first is the queue you are interested in, the second is a integer currently used as a boolean (0 or 1), which tells ServerKit if you want to wait for something to come or just return immediately either way. The third argument is a pointer to where you want to store the void * when you receive something.

Usually, you will want to set 1 for the boolean. This causes a wait in a pthread_cond_wait() behind the scenes for something to be added to the queue and call pthread_cond_signal() on the condition variable within the queue.

This approach works pretty well but note that it is moving individual units at a time, which can cause extraneous context switching. If you have a queue with high throughput there is another way to accept data from the queue in blocks rather than individually, the function is server_queue_acquire(). Using this method is slightly more complex because you must iterate over possibly many queue entries rather than just having a single void * stored for you. This also exposes you to the ServerKit heap part of the api, because the queues are built on top of them by default.

When you use server_queue_acquire, and walk the server_queue_entry_t based linked list, any entries you are finished with you have to explicitly free by calling server_heap_unit_unref(). If you don't do this in your consumer, you will just leak from the queues internal heap and eventually it's going to break, probably in the form of server_queue_push() just blocking indefinitely waiting for a heap unit. Forgetting to free also hurts the performance, you should return the queue entries as quickly as possible and avoid doing anything that can block while you have queue entries in hand, because it will force the queue heap to grow which is costly.

Note I didnt say never to block while you have queue entries, sometimes that is the whole point of putting a queue in front of what you are doing. Just keep it in mind to only do what is required before returning queue entries to the queue, and remember to implement timeouts.

There is one last function for working with the queues, that is server_queue_cycle(). This function is used when you want to trade an object with something on the queue if there is anything. This is used to implement the unfinished work feature mentioned above in the thread pools section.

You call this function with the queue you are interested in, and a pointer to the void * object you would like to give back to the queue. What it will do is if the queue has anything in it, it will take the first entry off, store the payload at the pointer you supplied, and take the void * your supplied pointer pointed at, storing it in the entry as the new payload. The entry is then placed at the back of the queue. If the queue is empty when you call server_queue_cycle(), nothing is done, your supplied void ** is left alone.

The server_queue_cycle() function is intended to be a form of "I'm sick of this one, maybe theres something better in there, I can use a break from this." So when there is something else for you to try out you get it, but if theres nothing else you are stuck with it carry on.

Of course depending on how your logic works and the task at hand, you may wind up with a queue of objects that you just keep cycling but you should never write code that cycles the queue without making any progress towards rendering the payload obsolete. Be sure to do some real work first, unless you're certain it makes sense.

The push, pull, acquire, and cycle functions are all atomic with regard to the queue. You must not free the queue while calling any of these functions, so your code must take care of that detail. You also must not call any of these functions after a queue has been freed, and a free attempt on a non-empty queue is considered a programming error and will fail loudly with an error.

2007-12-06