The client-side library for HyperDex uses an event loop, and is "thread-safe"
That is a common misconception. Or rather it is a tautalogy. No threads = thread-safe. But, it is not concurrently-modifying-data-structures safe -- which is the main painful point.
One can get just as easily tangled over a set of callbacks.
Here is a set of callbacks all started from some select/poll/epoll loop. Some call it a reactor (namely Glyph's own Twisted Python).
cb1 -> cb2 -> cb3|eb3 then cb3->cb4 and eb3->cb5
Processing starts with cb1 and ends with cb4 or cb5. Notice at some point cb2 function could result in generating an errback (eb3) which then ends up calling another callback cb5.
Understanding that the above, in a large system is just a messier, uglier concurrency structure than a thread/goroutine/task/actor is crucial.
It doesn't necessarily save you from simultaneous access to same shared data.
Imagine processing starts at cb1 and by the time it reaches cb3 (say cb2 calls some io or sleep operation), cb1 gets called again. cb1 through cb3 end up modifying some shared data (hey no need for lock, we are using callbacks remember!). Now there are two callback chains modifying shared data.
Yes you need locks and semaphores with the above just as you do with threads
For example this exists -- Twisted's own Sempahore:
I had to use it, and not just for throttling concurrency, but also to protect critical data from being modified concurrently.
Asynchronous/callback/promise/future based concurrency looks really good in small examples. In large application they get messy quickly.
Threads/actors/goroutines etc are still nicer from a logical, application point of view. You can even build them on top of the same epoll/select/kqueue system calls if the language can support some kind of a coroutine structure (which for example python gevent/eventlet) is doing.
I am unsure how to map your reply to my concept of HyperDex's event loop (and C API). You are of course correct, you have to synchronize access to some object which is a pain point.
What I am doing in my implementation of a thread-safe wrapper around HyperDex is similar to what you describe at the very end of your post.
The guarantee you get when using an event loop (or some other cooperative concurrency model) is that inside a contiguous block of code you don't have to worry about any other code accessing "your" data: until you yield control, the only code that will execute is your own.
Obviously if you call a function you need to be aware of what it might do to any state you grant it access to, but even then it can only mutate state either before it returns (as in most popular programming models even without concurrency) or, if it registers a callback, at some point after you yield control.
It takes a little bit of getting used to, but this effectively makes mutual exclusion blocks the default unit of execution - and in doing so eliminates a lot of the care needed to write correct code in a preemptive model. Take a look at a well written Node.js or Twisted app - neither Javascript nor Python has concurrent datastructures in their standard library, but it just isn't an issue in most cases.
All of that being said, I think that the failure of this model is that the reason programmers tend to yield control is not because they no longer need it, but rather because you they must do so in order to avoid blocking the event loop on I/O or a timer. In other words, they need to model arithmetic, for example, fundamentally differently than they model loading a configuration file. Things get even wonkier when you start specifying interfaces without knowledge of the implementation: at some point almost any operation could require IO, even if the default implementation doesn't, so you end up registering callbacks on every line. In addition to wreaking havoc on your mental model you have to begin worrying about stack sizes, etc.
In this sense, I think well written Go has a certain elegance. The programmer knows to treat shared state as a special and dangerous case. Instead, you tend to program around a produce/consume model using channels with near zero shared state. You don't need to worry about whether a call yields to the scheduler or not, or even how many threads your goroutines map to - you just write little blocks of code that operate on easily comprehensible state.
Comments
That is a common misconception. Or rather it is a tautalogy. No threads = thread-safe. But, it is not concurrently-modifying-data-structures safe -- which is the main painful point.
One can get just as easily tangled over a set of callbacks.
Here is a set of callbacks all started from some select/poll/epoll loop. Some call it a reactor (namely Glyph's own Twisted Python).
cb1 -> cb2 -> cb3|eb3 then cb3->cb4 and eb3->cb5
Processing starts with cb1 and ends with cb4 or cb5. Notice at some point cb2 function could result in generating an errback (eb3) which then ends up calling another callback cb5.
Understanding that the above, in a large system is just a messier, uglier concurrency structure than a thread/goroutine/task/actor is crucial.
It doesn't necessarily save you from simultaneous access to same shared data.
Imagine processing starts at cb1 and by the time it reaches cb3 (say cb2 calls some io or sleep operation), cb1 gets called again. cb1 through cb3 end up modifying some shared data (hey no need for lock, we are using callbacks remember!). Now there are two callback chains modifying shared data.
Yes you need locks and semaphores with the above just as you do with threads
For example this exists -- Twisted's own Sempahore:
http://twistedmatrix.com/documents/10.1.0/api/twisted.intern...
I had to use it, and not just for throttling concurrency, but also to protect critical data from being modified concurrently.
Asynchronous/callback/promise/future based concurrency looks really good in small examples. In large application they get messy quickly.
Threads/actors/goroutines etc are still nicer from a logical, application point of view. You can even build them on top of the same epoll/select/kqueue system calls if the language can support some kind of a coroutine structure (which for example python gevent/eventlet) is doing.
I am unsure how to map your reply to my concept of HyperDex's event loop (and C API). You are of course correct, you have to synchronize access to some object which is a pain point.
What I am doing in my implementation of a thread-safe wrapper around HyperDex is similar to what you describe at the very end of your post.
I just generalized about "thread safety" and async programming. So it was nothing specific to your particular code.
The guarantee you get when using an event loop (or some other cooperative concurrency model) is that inside a contiguous block of code you don't have to worry about any other code accessing "your" data: until you yield control, the only code that will execute is your own.
Obviously if you call a function you need to be aware of what it might do to any state you grant it access to, but even then it can only mutate state either before it returns (as in most popular programming models even without concurrency) or, if it registers a callback, at some point after you yield control.
It takes a little bit of getting used to, but this effectively makes mutual exclusion blocks the default unit of execution - and in doing so eliminates a lot of the care needed to write correct code in a preemptive model. Take a look at a well written Node.js or Twisted app - neither Javascript nor Python has concurrent datastructures in their standard library, but it just isn't an issue in most cases.
All of that being said, I think that the failure of this model is that the reason programmers tend to yield control is not because they no longer need it, but rather because you they must do so in order to avoid blocking the event loop on I/O or a timer. In other words, they need to model arithmetic, for example, fundamentally differently than they model loading a configuration file. Things get even wonkier when you start specifying interfaces without knowledge of the implementation: at some point almost any operation could require IO, even if the default implementation doesn't, so you end up registering callbacks on every line. In addition to wreaking havoc on your mental model you have to begin worrying about stack sizes, etc.
In this sense, I think well written Go has a certain elegance. The programmer knows to treat shared state as a special and dangerous case. Instead, you tend to program around a produce/consume model using channels with near zero shared state. You don't need to worry about whether a call yields to the scheduler or not, or even how many threads your goroutines map to - you just write little blocks of code that operate on easily comprehensible state.