4 ms·
Nitpick: mixing threads with pointers isn't really the problem; it's mixing threads with shared mutable state. A concurrency idiom I've gotten a lot of mileage
by chubot 13y ago
Nitpick: mixing threads with pointers isn't really the problem; it's mixing threads with shared mutable state.
A concurrency idiom I've gotten a lot of mileage out of is to use threads, without shared state, but with pointers. Instead of shared state, you use a message passing style where you pass pointers over queues. Threads only mutate data that they are explicitly handed via a queue. After putting an object on a queue, they NULL out their local reference.
It's basically like Go's goroutine idiom, but with C/C++/Python, real OS threads, and a thread safe queue (which is all a channel is). There is also some relation to the SEDA architecture.
The logical structure of such a program is not really that different than an Erlang program, I imagine. It just works a little better with existing C/C++ codebases, and is potentially faster in some cases.
It has a completely different character than spaghetti written using threads and shared state, and can be made extremely reliable. Especially because the explicit channels provide a hook for testing black box behavior.
- rdtsc 13y agoThat is true. One can build an almost shared nothing architecture in any of the languages supporting thread safe queues and threads. It is a continuum. One could write it with shared mutable state too, using locks. The problem is it is easy to mess up. C, a large program could load modules, and some of them are thread safe some are not. One could bring in a new module that calls some initialization routine (say curl_global_init()) and then another part of the system ends up calling it as well. But it is not thread safe and should only be called once. It is not intentional sharing, but it just happens as it is running in the same memory space. Someone likened that to running your code in Windows 3.1 when you Command & Conquer game would crash and take down the word processor with it. They didn't intend on doing so but it happened by accident as a bug. Sometimes running your application as a Windows 3.1 system is acceptable, sometimes it isn't. It depends on the problem.
- chubot 13y agoYeah, but the beauty of this approach is that it always makes architectural sense to put a given library in a single stage, and in that stage only. That library is enclosed in an "actor", and it communicates with the other stages/threads via an application-specific message (not using data types from the library; you want the loose coupling). If the library has non-reentrant code, then you can't have more than one thread for the stage -- you can't parallelize it. But it generally won't lead to correctness problems. curl is actually a great example, because it has an event loop for parallelism (rather than threads). So you would run a single thread for curl, because you wouldn't want more than one thread anyway. Say you are writing a web crawler. From the network/curl stage, you just pass off pointers to blocks of memory to parsing threads. Parsing threads will be CPU bound so you will likely want to run instances of that loop in multiple threads, and you will be able to do it with no problem, since they don't depend on the curl library. They just take in blocks of memory and output some data structure to another queue. This is also a good way to compose say the curl event loop with event loops from other libraries (GUI libraries, perhaps). Hence the relation to SEDA (http://en.wikipedia.org/wiki/Staged_event-driven_architecture http://en.wikipedia.org/wiki/Staged_event-driven_architectur...).
- e_proxus 13y agoI read your discussion and really get all the points, and still I just think "don't make me think." The thing with Erlang is, I don't have to care about threads, pointers, libraries, re-entrant code, thread safety etc. I can just write a couple of actors, who do one thing each (in parallel, because Erlang) and they can talk to each other, share some data if they want. The only thing I need to keep in mind is, who has what data (if I need it). I can hook up to the Erlang runtime (in local, staging, production, wherever) and talk to these actors. I can ask them "hey, what's your state now," "who are you talking to?" Sure, Erlang is slower in some cases, but for the value you get, I think it is priceless in many cases.
- chubot 13y agoSure, I wasn't saying not to use Erlang. Just saying that it is very possible to use threads and pointers in a sane (and Erlang-style) way. I am a big fan of interpreted languages and use them as my default. The situation I was describing was basically the only time I've ever rewritten in C++ for speed! It just doesn't happen that often. But I do wish the interpreted languages like Python, node.js, and Erlang all had better and more consistent C APIs (more like Lua's). C itself is not that hard, but the C APIs definitely put people off. Actually I wonder for Erlang, with the web crawler example, would you have to copy entire web pages if you wanted to pass them off from a "network actor" to a "parsing actor"? The threads + pointers solution easily avoids that, while retaining modularity.
- e_proxus 13y agoYes, Erlang has a "share nothing" concurrency model. You would need to send the data over to another process. There is one exception, which would come into play here, and that is binary data. Normal Erlang data ("Erlang terms") such as lists, tuples, strings, integers etc. are always copied, but binary data (raw binary data, but often representing string data) is just referenced. If you download a web page, you'd save the whole content as a binary, send it over to the other actor (and it would feel just as being "copied" because there is no difference compared to the other data types). Under the hood, Erlang would just pass the reference and handle everything for you (reference counting, garbage collection etc.). Also, when you split, or reference sub binaries, these become just pointers into the original binary data.
- tbrownaw 13y agoInstead of shared state, you use a message passing style where you pass pointers over queues. If you don't have a queue library and don't want to risk getting the locking wrong, you can get one from pipe(2). While simultaneous writes/reads are in theory permitted to be interleaved, the implementation would have to be actively insane to interleave anywhere other than page boundaries. ...I actually did this when I was at university, for the first class that covered locking and concurrency. Amazingly enough, I didn't fail that assignment for delegating what we were supposed to be learning to the OS. The logical structure of such a program is not really that different than an Erlang program, I imagine. It just works a little better with existing C/C++ codebases, and is potentially faster in some cases. It has a completely different character than spaghetti written using threads and shared state, and can be made extremely reliable. Maybe this is why I don't get all the C++-hate? The things that make code unreliable or thread-unsafe tend to also make it hard to read and think about (even without threads), so I just find some other way that doesn't give me a headache. Interestingly code that doesn't give me a headache tends to have a strong resemblance to functional-style code, even tho actual pure functional code also tends to be mildly headache-inducing.
- chubot 13y agoYup, pipe() is actually a great, minimal solution. POSIX guarantees PIPE_BUF to be at least 512 bytes, so you are guaranteed atomicity for any size pointer. I think on Linux the unit of atomicity is 4096 bytes, which is a page. Yeah, there are a million ways to use C++. To be fair, most people have their architecture forced upon them, rather than creating it from scratch. And the C++ world does have a culture of spaghetti with shared mutable state and threads. In Erlang you have the opposite culture. So I can understand the association that people make with a language, even though it's more a matter of culture. In addition, C++ doesn't even have a thread safe queue in the STL (although I was pointed to a draft for one in the next version of C++ by a coworker). pipe() is a good solution I would use, but not portable to other OSes.