4

I am trying to port to Linux an existing Windows C++ code that uses IOCP. Having decided to use epoll_wait to achieve high concurrency, I am already faced with a theoretical issue of when we try to process received data.

Imagine two threads calling epoll_wait, and two consequetives messages being received such that Linux unblocks the first thread and soon the second.

Example :

Thread 1 blocks on epoll_wait
Thread 2 blocks on epoll_wait
Client sends a chunk of data 1
Thread 1 deblocks from epoll_wait, performs recv and tries to process data
Client sends a chunk of data 2
Thread 2 deblocks, performs recv and tries to process data.

Is this scenario conceivable ? I.e. can it occure ?

Is there a way to prevent it so to avoid implementing synchronization in the recv/processing code ?

Mat
  • 202,337
  • 40
  • 393
  • 406
charfeddine.ahmed
  • 526
  • 2
  • 8
  • 16

5 Answers5

5

If you have multiple threads reading from the same set of epoll handles, I would recommend you put your epoll handles in one-shot level-triggered mode with EPOLLONESHOT. This will ensure that, after one thread observes the triggered handle, no other thread will observe it until you use epoll_ctl to re-arm the handle.

If you need to handle read and write paths independently, you may want to completely split up the read and write thread pools; have one epoll handle for read events, and one for write events, and assign threads to one or the other exclusively. Further, have a separate lock for read and for write paths. You must be careful about interactions between the read and write threads as far as modifying any per-socket state, of course.

If you do go with that split approach, you need to put some thought into how to handle socket closures. Most likely you will want an additional shared-data lock, and 'acknowledge closure' flags, set under the shared data lock, for both read and write paths. Read and write threads can then race to acknowledge, and the last one to acknowledge gets to clean up the shared data structures. That is, something like this:

void OnSocketClosed(shareddatastructure *pShared, int writer)
{
  epoll_ctl(myepollhandle, EPOLL_CTL_DEL, pShared->fd, NULL);
  LOCK(pShared->common_lock);
  if (writer)
    pShared->close_ack_w = true;
  else
    pShared->close_ack_r = true;

  bool acked = pShared->close_ack_w && pShared->close_ack_r;
  UNLOCK(pShared->common_lock);

  if (acked)
    free(pShared);
}
bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • So same as suggested by StitchedUp, this appears to be the best solution. However I ma sad that write events and read events can not be processed in // – charfeddine.ahmed Apr 07 '11 at 20:25
  • @charfeddine, you could have separate threads for write and read events, each using a different epoll handle with watches only on write or read, not both. This would allow you to parallelize write and read responses, although I tend to doubt it would make a big difference in performance. – bdonlan Apr 07 '11 at 20:39
  • How is that please ? When I read the Linux Programmer manual, I only learn that the act of rearming the socket applies to the socket file descriptor as a whole. In my application this will make a major difference : server responses are not necessarily piloted by client requests : it is gonna be noticeable defect that server asynchronous broadcast information pause whe client sends something. – charfeddine.ahmed Apr 07 '11 at 22:40
  • Updated my answer with something perhaps a bit better attuned to what you need – bdonlan Apr 08 '11 at 01:39
  • You are really rigourous. OK that makes me recall the same issue with IOCP, mainly the obligation to record outstanding IOs and when the connection is closed, the removal of contextual information should be delayed until the count of all those IOs goes down to 0. However regarding how to split read/write processing, do you mean using two epoll handles ? or rather same epoll, but two input handles for same epoll, ie something like this code I put in this picture https://lh4.googleusercontent.com/_5sK5LKdcP34/TZ7Wsiujw6I/AAAAAAAAAqI/GO4-5aafCSA/s640/Epoll.png ? – charfeddine.ahmed Apr 08 '11 at 09:36
  • You can only have one entry per fd per epoll, so use two epoll handles. – bdonlan Apr 08 '11 at 18:41
  • I do not believe you. At least I can tell that the code I posted in the picture is working ! – charfeddine.ahmed Apr 08 '11 at 20:13
  • 1
    @charfeddine: you are not checking for errors. The second epoll_mod will (and does, I just tested) fail with EEXIST; you will not easily notice if you're attempting to send immediately before queueing up data for write, but you will never get a write-ready notification with this. – bdonlan Apr 09 '11 at 03:00
3

I'm assuming here that the situation you're trying to process is something like this:

You have multiple (maybe very many) sockets that you want to receive data from at once;

You want to start processing data from the first connection on Thread A when it is first received and then be sure that data from this connection is not processed on any other thread until you have finished with it in Thread A.

While you are doing that, if some data is now received on a different connection you want Thread B to pick that data and process it while still being sure that no one else can process this connection until Thread B is done with it etc.

Under these circumstances it turns out that using epoll_wait() with the same epoll fd in multiple threads is a reasonably efficient approach (I'm not claiming that it is necessarily the most efficient).

The trick here is to add the individual connections fds to the epoll fd with the EPOLLONESHOT flag. This ensures that once an fd has been returned from an epoll_wait() it is unmonitored until you specifically tell epoll to monitor it again. This ensures that the thread processing this connection suffers no interference as no other thread can be processing the same connection until this thread marks the connection to be monitored again.

You can set up the fd to monitor EPOLLIN or EPOLLOUT again using epoll_ctl() and EPOLL_CTL_MOD.

A significant benefit of using epoll like this in multiple threads is that when one thread is finished with a connection and adds it back to the epoll monitored set, any other threads still in epoll_wait() are immediately monitoring it even before the previous processing thread returns to epoll_wait(). Incidentally that could also be a disadvantage because of lack of cache data locality if a different thread now picks up that connection immediately (thus needing to fetch the data structures for this connection and flush the previous thread's cache). What works best will sensitively depend on your exact usage pattern.

If you are trying to process messages received subsequently on the same connection in different threads then this scheme to use epoll is not going to be appropriate for you, and an approach using a listening thread feeding an efficient queue feeding worker threads might be better.

StitchedUp
  • 96
  • 5
  • Excellent ! Eventhough I am not confortable with the idea that this will block the write event from being processed in // with read event. – charfeddine.ahmed Apr 07 '11 at 20:11
  • It is possible to process read and write in parallel by using the dup() syscall to duplicate the fd and then registering one fd with EPOLLIN and the other with EPOLLOUT. However you have now used twice the number of fds and for applications that are ment to process many connections they tend to be a limited resource. – StitchedUp Apr 11 '11 at 19:54
2

Previous answers that point out that calling epoll_wait() from multiple threads is a bad idea are almost certainly right, but I was intrigued enough by the question to try and work out what does happen when it is called from multiple threads on the same handle, waiting for the same socket. I wrote the following test code:

#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

struct thread_info {
  int number;
  int socket;
  int epoll;
};

void * thread(struct thread_info * arg)
{
    struct epoll_event events[10];
    int s;
    char buf[512];

    sleep(5 * arg->number);
    printf("Thread %d start\n", arg->number);

    do {
        s = epoll_wait(arg->epoll, events, 10, -1);

        if (s < 0) {
            perror("wait");
            exit(1);
        } else if (s == 0) {
            printf("Thread %d No data\n", arg->number);
            exit(1);
        }
        if (recv(arg->socket, buf, 512, 0) <= 0) {
            perror("recv");
            exit(1);
        }
        printf("Thread %d got data\n", arg->number);
    } while (s == 1);

    printf("Thread %d end\n", arg->number);

    return 0;
}

int main()
{
    pthread_attr_t attr;
    pthread_t threads[2];
    struct thread_info thread_data[2];
    int s;
    int listener, client, epollfd;
    struct sockaddr_in listen_address;
    struct sockaddr_storage client_address;
    socklen_t client_address_len;
    struct epoll_event ev;

    listener = socket(AF_INET, SOCK_STREAM, 0);

    if (listener < 0) {
        perror("socket");
        exit(1);
    }

    memset(&listen_address, 0, sizeof(struct sockaddr_in));
    listen_address.sin_family = AF_INET;
    listen_address.sin_addr.s_addr = INADDR_ANY;
    listen_address.sin_port = htons(6799);

    s = bind(listener,
             (struct sockaddr*)&listen_address,
             sizeof(listen_address));

    if (s != 0) {
        perror("bind");
        exit(1);
    }

    s = listen(listener, 1);

    if (s != 0) {
        perror("listen");
        exit(1);
    }

    client_address_len = sizeof(client_address);
    client = accept(listener,
                    (struct sockaddr*)&client_address,
                    &client_address_len);

    epollfd = epoll_create(10);
    if (epollfd == -1) {
        perror("epoll_create");
        exit(1);
    }

    ev.events = EPOLLIN;
    ev.data.fd = client;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client, &ev) == -1) {
        perror("epoll_ctl: listen_sock");
        exit(1);
    }

    thread_data[0].number = 0;
    thread_data[1].number = 1;
    thread_data[0].socket = client;
    thread_data[1].socket = client;
    thread_data[0].epoll = epollfd;
    thread_data[1].epoll = epollfd;

    s = pthread_attr_init(&attr);
    if (s != 0) {
        perror("pthread_attr_init");
        exit(1);
    }

    s = pthread_create(&threads[0],
                       &attr,
                       (void*(*)(void*))&thread,
                       &thread_data[0]);

    if (s != 0) {
        perror("pthread_create");
        exit(1);
    }

    s = pthread_create(&threads[1],
                       &attr,
                       (void*(*)(void*))&thread,
                       &thread_data[1]);

    if (s != 0) {
        perror("pthread_create");
        exit(1);
    }

    pthread_join(threads[0], 0);
    pthread_join(threads[1], 0);

    return 0;
}

When data arrives, and both threads are waiting on epoll_wait(), only one will return, but as subsequent data arrives, the thread that wakes up to handle the data is effectively random between the two threads. I wasn't able to to find a way to affect which thread was woken.

It seems likely that a single thread calling epoll_wait makes most sense, with events passed to worker threads to pump the IO.

Al Riddoch
  • 583
  • 2
  • 9
  • In my case I want to spawn multiple threads. Previous ideas suggesting to use one thread that in turn pumps data to another queue are not suitable for me for multiple reasons. – charfeddine.ahmed Apr 07 '11 at 20:28
1

I believe that the high performance software that uses epoll and a thread per core creates multiple epoll handles that each handle a subset of all the connections. In this way the work is divided but the problem you describe is avoided.

Zan Lynx
  • 53,022
  • 10
  • 79
  • 131
  • 2
    Ok the problem is avoided, but inefficiency will arise, if there is a big difference in activity between different sets of sockets being managed by different threads. – charfeddine.ahmed Apr 07 '11 at 20:21
0

Generally, epoll is used when you have a single thread listening for data on a single asynchronous source. To avoid busy-waiting (manually polling), you use epoll to let you know when data is ready (much like select does).

It is not standard practice to have multiple threads reading from a single data source, and I, at least, would consider it bad practice.

If you want to use multiple threads, but you only have one input source, then designate one of the threads to listen and queue the data so the other threads can read individual pieces from the queue.

Jonathan
  • 13,354
  • 4
  • 36
  • 32
  • 1
    Why is it not a standard practise ? I've seen many project spawning multiple threads that do that call. Does epoll not behave like a Multiple Producers **Multiple** Consumers queue ? – charfeddine.ahmed Apr 07 '11 at 20:19
  • As I've seen it used, it was for handling asynchronous access to multiple streams by a single thread. I don't have much experience with it, though, so it is entirely possible that what I saw was uncommon use; if so, I apologize for my ignorance. – Jonathan Apr 08 '11 at 13:27