Tuesday, January 24, 2012

A simple player simulation using pthreads

When I was playing VLC, just wondering how pause, play and resume works. I decided to simulate one using pthreads. This is just done for fun nothing official. So I used the wait and resume calls of pthreads.

1) pthread_cond_wait()
2) pthread_cond_signal()

Using above two POSIX calls I did a simulation of player pause and resume. It worked as expected. So it was done as below

1) Player is executed as a separate thread
2) Button simulation done using command line in main thread.
3) When pause is opted, pthread_cond_wait() is called to make thread wait.
4) When resume is opted, pthread_cond_signal() is called to wake up the waiting thread.

As far as wake up is concerned, there is one more variant pthread_cond_broadcast() which wakes up all threads waiting on condition variable. Please refer man pages to know detailed usage of pthread calls. Not sure about internals may be the futex is released atomically and wakeup** is called for this task (again not sure)

Here is C code, not completely tested however giving what is required. Suggestions and further improvements are always welcome.

#include<stdio.h>
#include<unistd.h>
#include<termios.h>
#include<stdlib.h>
#include<pthread.h>

typedef enum {
    PLAY,
    PAUSE,
    STOP,
}player_state;

struct player {
    pthread_mutex_t player_mutex;
    pthread_cond_t player_cond;
    pthread_t player_tid;
    player_state player_s;
};

static void init_player(struct player *__arg);
static void print_player_menu(void);
static int start_player(struct player *__arg);
static int pause_player(struct player *__arg);
static int resume_player(struct player * __arg, const player_state player_s);
static int stop_player(struct player *__arg);
static char read_char(void);
void* start_player_thread(void *__arg);

int main()
{
    struct player bazooka_player;
    init_player(&bazooka_player);
    print_player_menu();

    while (1) {
        char c = read_char();

        switch (c) {
            case 'S':
            case 's':
                if (bazooka_player.player_s == STOP) {
                    start_player(&bazooka_player);
                } else {
                    printf("Invalid State to call player start\n");
                }
                break;

            case 'P':
            case 'p':
                if (bazooka_player.player_s == PLAY) {
                    pause_player(&bazooka_player);
                } else {
                    printf("Invalid State to call player pause\n");
                }
                break;

            case 'R':
            case 'r':
                if (bazooka_player.player_s == PAUSE) {
                    resume_player(&bazooka_player, PLAY);
                } else {
                    printf("Invalid State to call player resume\n");
                }
                break;

            case 'Q':
            case 'q':
                stop_player(&bazooka_player);
                exit(EXIT_SUCCESS); //Any better way?
                break;

            default:
                printf("Invalid player option\n");
                break;
        }
    }
}

static void init_player(struct player *__arg)
{
    /*
     * Make sure you do proper initialization
     * Otherwise pthread_cond_signal() would result in deadlock
     */
    __arg->player_s = STOP;
    __arg->player_tid = -1;
    pthread_cond_init(&__arg->player_cond, NULL);
    pthread_mutex_init(&__arg->player_mutex, NULL);
}

static char read_char(void)
{
    /*
     * This is taken from net.
     * It basically turns of echo for the current terminal.
     * Takes single character without expecting new line.
     * Restores old tty settings.
     * returns character.
     */
    char c;
    struct termios old_term, new_term;
    tcgetattr(STDIN_FILENO, &old_term);
    new_term = old_term;
    new_term.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &new_term);
    c = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &old_term);
    return c;
}

static void print_player_menu(void)
{
    printf("PLAYER OPTIONS\n");
    printf("S or s: Start play\n");
    printf("P or p: Pause play\n");
    printf("R or r: Resume play\n");
    printf("Q or q: Stop play\n");
}

static int start_player(struct player *__arg)
{
    /*
     * This could get racy if we set state to PLAY after pthread_create().
     * Do we need explicit memory barrier in multi-core platform?
     * smp_wmb()? Not sure :-)
     */
    __arg->player_s = PLAY;
    return pthread_create(&__arg->player_tid, NULL, start_player_thread, __arg);
}

static int pause_player(struct player *__arg)
{
    __arg->player_s = PAUSE;
}

static int resume_player(struct player *__arg, const player_state player_s)
{
    printf("Preparing to resume\n");
    __arg->player_s = player_s;
    /*
     * Again Do we need smp_wmb()?
     */
    return pthread_cond_signal(&__arg->player_cond);
}

static int stop_player(struct player *__arg)
{
    /*
     * This may hang the thread if the player is paused.
     * We will have to explicitly do signal in case, player is paused
    */

    if (__arg->player_s == PAUSE) {
        resume_player(__arg, STOP);
    } else {
        __arg->player_s = STOP;
    }

    return ((__arg->player_tid == -1)?0:pthread_join(__arg->player_tid, NULL));
}

void* start_player_thread(void* __arg)
{
    struct player* pl_s = (struct player*) __arg;
    while (1) {
        switch (pl_s->player_s) {
            case PLAY:
                printf("playing the song :-)\n");
                sleep(1); //Not sure how much safe to call this inside a thread
            break;

            case PAUSE:
                printf("preparing to pause :-)\n");
                pthread_mutex_lock(&pl_s->player_mutex);
                pthread_cond_wait(&pl_s->player_cond, &pl_s->player_mutex);
                pthread_mutex_unlock(&pl_s->player_mutex);
            break;

            case STOP:
                printf("good bye! Hope you enjoyed it :-)\n");
                pthread_exit(NULL);
            break;   
        }
    }
}


Compile with: gcc -o <binary_name> <saved_c_file> -lpthread
Execute with: ./<binary_name> and test output