[PATCH] client: Add acquire-fd API to avoid requiring a polling main thread

Uli Schlachter psychon at znc.in
Mon Mar 25 11:49:32 PDT 2013


On 25.03.2013 18:42, Kristian Høgsberg wrote:
[...]
> @@ -847,45 +973,105 @@ dispatch_event(struct wl_display *display, struct wl_event_queue *queue)
>  }
>  
>  static int
> -dispatch_queue(struct wl_display *display,
> -	       struct wl_event_queue *queue, int block)
> +read_events(struct wl_display *display, struct wl_event_queue *queue)
>  {
> -	int len, size, count, ret;
> -
> -	pthread_mutex_lock(&display->mutex);
> -
> -	if (display->last_error)
> -		goto err_unlock;
> +	struct pollfd pfd[2];
> +	int len, size, ret;
> +	char c;
> +
> +	if (display->reader_state == NO_READER) {
> +		/* A thread gets here when it's the first reader to
> +		 * try to read and there's LOCKED_READER.  We set
> +		 * VOLUNTEER_READER and poll on the fd and the
> +		 * reader_pipe, so that a locked reader can preempt if
> +		 * it needs to. */
> +		display->reader = pthread_self();
> +		display->reader_state = VOLUNTEER_READER;
> +
> +		pthread_mutex_unlock(&display->mutex);
> +
> +		pfd[0].fd = display->fd;
> +		pfd[0].events = POLLIN;
> +		pfd[1].fd = display->reader_pipe[0];
> +		pfd[1].events = POLLIN;
> +		ret = poll(pfd, 2, -1);
> +		if (pfd[1].revents & POLLIN)
> +			read(display->reader_pipe[0], &c, 1);
> +
> +		pthread_mutex_lock(&display->mutex);
> +
> +		pthread_cond_signal(&display->pipe_cond);
> +
> +		if (ret == -1) {
> +			display_fatal_error(display, errno);
> +			return -1;
> +		}
>  
> -	ret = wl_connection_flush(display->connection);
> -	if (ret < 0 && errno != EAGAIN) {
> -		display_fatal_error(display, errno);
> -		goto err_unlock;
> +		/* If we come back from poll in NO_READER state,
> +		 * another thread locked and then released the fd.
> +		 * Nobody is going to read anything in this state
> +		 * so return instead of blocking without a reader. */
> +		if (display->reader_state == NO_READER)
> +			return 0;

This can no longer happen, either the reader_state is VOLUNTEER_READER or
LOCKED_READER at this point (right?).

>  	}
>  
> -	if (block && wl_list_empty(&queue->event_list) &&
> -	    pthread_equal(display->display_thread, pthread_self())) {
> +	if (pthread_equal(display->reader, pthread_self())) {
> +		/* At this point we have a reader.  Either a
> +		 * VOLUNTEER_READER and the fd is readable or a
> +		 * LOCKED_READER which is allowed to block in
> +		 * wl_connection_read(). */
>  		len = wl_connection_read(display->connection);
>  		if (len == -1) {
>  			display_fatal_error(display, errno);
> -			goto err_unlock;
> +			return -1;
>  		} else if (len == 0) {
>  			display_fatal_error(display, EPIPE);
> -			goto err_unlock;
> +			return -1;
>  		}
>  		while (len >= 8) {
>  			size = queue_event(display, len);
>  			if (size == -1) {
>  				display_fatal_error(display, errno);
> -				goto err_unlock;
> +				return -1;
>  			} else if (size == 0) {
>  				break;
>  			}
>  			len -= size;
>  		}
> -	} else if (block && wl_list_empty(&queue->event_list)) {
> -		pthread_cond_wait(&queue->cond, &display->mutex);
> -		if (display->last_error)
> +
> +		/* Indicate we're done by setting NO_READER and wake
> +		 * up other threads waiting to read. */
> +		if (display->reader_state != LEGACY_READER)
> +			display->reader_state = NO_READER;
> +		pthread_cond_broadcast(&display->reader_cond);
> +	} else {
> +		/* Some other thread signed up to read from the fd, so
> +		 * just wait for the reader to do the work. */
> +		pthread_cond_wait(&display->reader_cond, &display->mutex);
> +	}

Ok, I think I found another problem:

- Thread 1 is a volunteer reader, sitting in poll()
- The server sends some data and poll() returns because display->fd is readable
- Thread 2 calls wl_display_acquire_fd() and reaches the cond_wait(pipe_cond)
- Thread 1 reaches the above cond_wait(reader_cond)
- Thread 2 normally reads from the connection and wakes up all readers.

At this point, everything is OK. No thread has any problems. However, there is
still a single byte sitting in the pipe that was not read. So what happens next?

- Thread X wants to read from the display and becomes a volunteer reader. Since
the pipe is readable, it immediately continues, reads the byte and ends up in
the above cond_wait(reader_cond). At this point we have a thread waiting on
reader_cond, but no reader which will wake it up (=exactly the situation that
inspired this patch, just a lot more unlikely).

Of course, all of this is highly unlikely to happen, but it might happen
nevertheless. :-)

My suggestion would be to move the read() into wl_display_acquire_fd() and have
the VOLUNTEER_READER only exit from poll() without reading from the pipe.

So wl_display_acquire_fd() would do:

    if (old_state == VOLUNTEER_READER) {
        write(display->reader_pipe[1], &c, 1);
        pthread_cond_wait(&display->pipe_cond, &display->mutex);
        read(display->reader_pipe[0], &c, 1);
    }

> +	return 0;
> +}
> +
> +static int
> +dispatch_queue(struct wl_display *display,
> +	       struct wl_event_queue *queue, int block)
> +{
> +	int ret, count;
> +
> +	pthread_mutex_lock(&display->mutex);
> +
> +	if (display->last_error)
> +		goto err_unlock;
> +
> +	ret = wl_connection_flush(display->connection);
> +	if (ret < 0 && errno != EAGAIN) {
> +		display_fatal_error(display, errno);
> +		goto err_unlock;
> +	}
> +
> +	if (block && wl_list_empty(&queue->event_list)) {
> +		if (read_events(display, queue) == -1)
>  			goto err_unlock;
>  	}
>  
> @@ -965,7 +1151,8 @@ wl_display_dispatch_queue_pending(struct wl_display *display,
>   * or not. For dispatching main queue events without blocking, see \ref
>   * wl_display_dispatch_pending().
>   *
> - * \note Calling this makes the current thread the main one.
> + * \note Calling this will release the display file descriptor if this
> + * thread acquired it using wl_display_acquire_fd().
>   *
>   * \sa wl_display_dispatch_pending(), wl_display_dispatch_queue()
>   *
> @@ -974,7 +1161,8 @@ wl_display_dispatch_queue_pending(struct wl_display *display,
>  WL_EXPORT int
>  wl_display_dispatch(struct wl_display *display)
>  {
> -	display->display_thread = pthread_self();
> +	if (display->reader_state == LEGACY_READER)
> +		display->reader = pthread_self();

Doesn't this comparison need to be protected? This could race with another
thread calling wl_display_lock_fd() and changing reader_state away from
LEGACY_READER?

However, I guess this is no issue, because only the "main thread" is allowed to
lock the fd and dispatch the display, right?

>  	return dispatch_queue(display, &display->queue, 1);
>  }
> @@ -1020,7 +1208,8 @@ wl_display_dispatch(struct wl_display *display)
>  WL_EXPORT int
>  wl_display_dispatch_pending(struct wl_display *display)
>  {
> -	display->display_thread = pthread_self();
> +	if (display->reader_state == LEGACY_READER)
> +		display->reader = pthread_self();

Same as above.

>  	return dispatch_queue(display, &display->queue, 0);
>  }
> diff --git a/src/wayland-client.h b/src/wayland-client.h
> index 578fa7e..2efd01c 100644
> --- a/src/wayland-client.h
> +++ b/src/wayland-client.h
> @@ -152,6 +152,9 @@ int wl_display_flush(struct wl_display *display);
>  int wl_display_roundtrip(struct wl_display *display);
>  struct wl_event_queue *wl_display_create_queue(struct wl_display *display);
>  
> +int wl_display_acquire_fd(struct wl_display *display);
> +void wl_display_release_fd(struct wl_display *display);
> +
>  void wl_log_set_handler_client(wl_log_func_t handler);
>  
>  #ifdef  __cplusplus


-- 
- Buck, when, exactly, did you lose your mind?
- Three months ago. I woke up one morning married to a pineapple.
  An ugly pineapple... But I loved her.


More information about the wayland-devel mailing list