C++ is still a fantastic language. It's hard to learn to use it effectively but once you do you have one of the most powerful programming tools available under your belt. The two main points to using C++ effectively are 1. you must protect allocated resources with smart pointers and 2. threads should only be used to exercise multiple CPU cores and never as a way to work around blocking IO.
That later point is a little more complicated/subtle and took me years of threaded programming to learn and accept. When adhering to these two rules, C++ is safe and extremely powerful.
i think this is strange. i'm not a c++ programmer, but the evented frameworks (node.js or vert.x) are doing it the other way round: use threads only for IO (or computation so heavy it's practically blocking). the complete opposite.
ah, well - when reading "blocking io" i first thought about the file system. so does my point still stand? using a threadpool for disk IO does go against "threads should only be used to exercise multiple CPU cores and never as a way to work around blocking IO."
2.a. is about performance. And while not specifically about C++ this article covers the main idea http://www.kegel.com/c10k.html
2.b. is about avoiding deadlocks and race conditions. Threaded programming in C++ is dangerous. For years I would have said, that I could avoid deadlocks and race conditions by virtue of being a good programmer. However, I've debugged enough of my own code to know this is not always true.
If you only use threads for data driven computation, then you greatly reduce the risks. On the other hand if you have multiple threads running around accessing anything and everything in your program then you need a lot of thread locks which is tedious to implement and not well supported in C++ (vs. say the synchronized keyword in Java), does not perform well and still it is very easy to make mistakes. The point is that there is no reason to let threads run willy nilly because you can do event driven programming in C++ (libev, libevent, libuv) and most programs (or rather most parts of a program) are not computationally intensive.
However, C++ really isn't that hard to learn. Harder to learn than some alternatives, perhaps, but only like driving a stick shift is harder than an AT, but neither really qualifies as simply hard.
I bet I could name a few things about C++ you probably don't know. One small example, with out looking it up can tell me how to add a prefix operator (e.g. ++) to a class vs. a postfix operator? The answer is ugly. There are a lot details to C++ (and I'm only talking about C++98 at this point). It's easy to learn enough C++ to create some programs but it's hard to really learn C++.
Yes. And even if I didn't, it would be easy enough to look up, and there's no danger to me for not knowing it. That's not where C++'s learning curve has its sharp edges.
This statement misses the point. What I am saying is only use threads to compute things simultaneously and not to perform IO operations simultaneously.
I understand what you saying but its too strict. Say the things you want to compute simultaneously involve IO. For example your application has a window that displays a log by watching for writes to the end of a log file (like tail). This is easy and reasonable to do in a thread.
#2 seems questionable. I always program in a thread-per-request style and start threads for blocking sub-operations of top level requests as well. I know there are platforms where this doesn't work well and I also know a lot of people cut their teeth on dreadful systems like sunos where it took ten seconds to start a thread, but on modern Linux threads are dirt cheap and well worth the clarity that synchronous programming allows.
A context switch takes about 50 microseconds. If you want to handle 10,000 requests in a second 500 milliseconds is lost to context switching alone. Probably more. The nature of IO & user input in general is to hurry up and wait. It's usually simpler to do async requests & polling file descriptors rather than thread-per-request. The former seems on the surface more complicated and more lines of code, but IMO, ends up being less complicated because you won't have to spend time digging through logs trying to figure out that the reason some other unrelated thread stalled for so long is because you were blasted with thousands of requests in the same second.
Where did you get that number? That's the kind of SunOS derangement I'm talking about. On Linux with NPTL (which is over a decade old) and any recent Intel CPU (Westmere or later) a context switch between threads of the same process takes only about 1-2 microseconds.
Regardless, context switching, waiting on locks and the added cost of allocating a stack to each thread are all relatively expensive. I've gotten huge performance increases by rewriting threaded code to instead use non-blocking IO.
I think these can be and often are combined. You can definitely have an event driven main IO loop accepting connections and reading packets, that nevertheless dispatches whole requests onto their own threads. I work with a lot of services written that way. The fact that you can reuse the frontend for several applications amortizes the cost of developing it as a high performance asynchronous thing.
As for stack allocation I definitely see your point there. It is bizarre that Linux defaults to 2MB or whatever it is these days. I usually work with a 64KB thread stack.
I won't argue that async software thread per hardware thread style will have better maximum performance than a thread-per-request style, but I think that difference will not be material in most applications (the difference between the two styles of C++ server programming being far smaller than the difference between, say, a Go program and a C++ program), and the cost paid in code readability is pretty high. All that async continuation passing makes your code a twisty maze, and that style is fraught with lifetime problems that are the very thing most C++ haters are complaining about. In synchronous style all the allocations are scoped by nature. In async style the programmer has to assign ownership of everything to some master continuation that will delete them all in the final callback. That adds a lot of code.
You are right about the added difficulty of resource ownership in the face of asynchronous code. This is a trade off. lambda functions in C++11 can help with this. They make it possible to pass resource allocations (smart pointers) through to callbacks.
> On the contrary, it gives your code clarity, consistency and predictability.
Well, when a product I worked on gradually migrated from event loops with callbacks to threads, clarity, consistency, and predictability made big improvements. Some callback usage would have been better with C++11 or C++14, but it's still way worse than what you can do simply by having variables go in and out of scope on a stack.
Synchronous looking programming doesn't offer any clarity for anything beyond dumb sequential requests to a single node, that nobody cares about. Try building a distributed k/v store with that.
It fails miserably once you have to deal with zero downtime restarts for continuous deployment, rate limiting, requests cancellations, waiting for one or multiple responses from multiple nodes, making more requests to other nodes just after a bit of time to guarantee latency, using a lot of different kinds of shared data, like response queues, log queues, connection caches, data caches, all used by multiple clients, while at the same time doing some synchronization of data between nodes, etc.
Anything non-trivial is impossible with threads in practice.
These statements are so cute. I can reasonably assure you that the distributed k/v store I have written in the thread-per-request style is currently out there serving more traffic than you've ever dreamed of.
As for your specific points, I disagree totally. Each of those things is easier to me in the synchronous style than in the async style. Certainly something like hedging is way easier.
If you're talking to disk, you'll eventually be doing it synchronously anyway, because on Linux the API's for async disk I/O aren't (weren't?) all there yet. For some time I was working on a product with a command line flag to decide between using a worker pool of threads versus event-based disk I/O, and there wasn't a big enough performance difference (if any, I don't remember) for us to keep the event-based code. If you're getting some fancy PCI SSD's or persistent RAM and really want to minimize latency-to-fsync time, maybe things would be different.
Also, if you use an event loop, you have to do scheduling yourself. For distributed key/value stores, good thread scheduling is even more important. It removes a big amount of technical risk to lean on the kernel's, unless your product is sufficiently minimalistic.
Async will get you real performance advantages for pure networking, but if you're talking to disk it's, when not buggy, not very useful anyway.
Also, as for programming style, even if you want event-based syscalls, cooperative userland-managed threads are a big win over callback hell. The only bad thing about them is wasting address-space on 32-bit systems, and maybe just wasting memory too.
Cooperative threads only introduce confusion and are not making anything easier over threads. For example, the following code will fail if one of the functions yields and global array index changes by another cooperative thread:
There is no protection from this, synchronization primitives are still needed, just like with threads.
Which is not the case with event-driven approach. Event-driven code forces you to write asynchronous functions differently from the rest. There is no confusion of what yields where and returns from where. There are just continuations. So, if foo() is asynchronous in the example above, you wouldn't modify global arr_index across callbacks, you either store its value or modify it in a single callback, because you can rely on callbacks to never get interrupted:
foo(func(res){
arr_index ++
arr[arr_index].foo = res
arr[arr_index].bar = bar()
})
Furthermore, because of the very specific constraints on callbacks, event-driven approach allows to implement some sort of composable virtual processes, capable of cleaning up and keeping track of various resources and accepting signals from other virtual processes via callback. And it's pretty easy to do, so some event-driven applications in the wild do that, although not always call them processes.
So, again, if you find event-driven programming in any way harder, than multithreaded - you are definitely doing it wrong. Because it completely frees you from thinking about synchronization without adding anything extra that you cannot get used to.
As for disks: there is more than one and you are not talking to them directly, there is a filesystem cache in between. Event loops typically use thread pools to fake asynchronous filesystem access and for other work. And it makes some sense latency and performance-wise, because request that waits for some disk does not block requests that want to access other disks, or hit cache, or maybe it's an ssd and accessing it in parallel is faster.
> Cooperative threads only introduce confusion and are not making anything easier over threads.
They introduce (ostensibly or in reality) performance, of the kind that obviates a mass of arguments in favor of event loops.
> There is no protection from this, synchronization primitives are still needed, just like with threads.
And it's not a problem.
> There is no confusion of what yields where and returns from where.
This isn't a problem in practice, except when you're (without due care, but it's easy to screw up) migrating from the event-based code that was written with this assumption. You write your code as if it could block anywhere, and then stop wondering whether something could block. If some code is totally not going to block, you use a mutex anyway, hopefully the asserting kind that logs a message if it ever has to wait.
It's easy for code written with callbacks to wind up with baked in, undocumented dependencies of chunks of code that aren't allowed to be split up is far worse to maintain than that which lays out that information explicitly.
Your example with arr_index is a great demonstration of something trivially solvable by protecting the shared field with a mutex. And if you did that, you would easily notice if foo() or bar() ever decided it should increment arr_index.
> Because it completely frees you from thinking about synchronization
Well this is untrue. So completely untrue!
(Edit: Hell, you very nearly showed it in your own examples. Make the first foo() run your foo(func...) example, make that callback get called without waiting in some cases, and bam, you've got a synchronization problem.
Callbacks do nothing to protect you against the class of synchronization problems where the order in which you do stuff matters. For example, naively written rate-limiting code using callbacks will allow incoming actions to pass through out-of-order and might also starve operations that had been blocked. Likewise, you are unprotected from that variable getting incremented when a callback gets called sooner than you'd expect -- and no, on some supposedly high-performance server under load, throwing every callback around the event loop an extra time is not an acceptable option.
Ultimately it's moot -- for sane enforcement of proper ordering, you have to rely on some sort of explicit mutual exclusion.)
> some sort of composable virtual processes, capable of cleaning up and keeping track of various resources and accepting signals from other virtual processes via callback.
And what if I told you that you can keep track of resources and accept signals from, well, anything, in cooperative (or non-cooperative) threads? Because, you can.
> As for disks: there is more than one and you are not talking to them directly, there is a filesystem cache in between.
Sometimes you are talking to them directly. Event loops use thread pools for disk I/O (instead of the event-based APIs on some platforms) because, well, I don't know what other peoples' motivations were, but I can tell you it's more portable and kernels seem less buggy when you do it that way.
> for sane enforcement of proper ordering, you have to rely on some sort of explicit mutual exclusion.
Oh, see, we got somewhere. You seem to presume that it's ok to implement an event loop that doesn't guarantee mutual exclusivity of callbacks. But that's not the case, the whole point of event-driven programming is to guaranty that exclusivity, so you wouldn't have to think about synchronization. So, your arguments do not apply here at all.
EDIT:
> example, make that callback get called without waiting in some cases, and bam, you've got a synchronization problem
There is no such thing as waiting for callbacks in event loops. There is giving up control to a callback and getting it back once it returns. Or it's not an event loop otherwise.
> And what if I told you that you can keep track of resources and accept signals from, well, anything, in cooperative (or non-cooperative) threads? Because, you can.
No, you can't. Because of the way blocking code is written there is no guaranteed state of mutual exclusivity of everything where your signal/message handler can execute and operate in the same scope, without causing havoc.
> You seem to presume that it's ok to implement an event loop that doesn't guarantee mutual exclusivity of callbacks.
No, that's not what I said at all. How could you even infer that? What I was talking about was, try imagining how you'd enforce a certain ordering of behaviors. For example, a series of requests come in, they're numbered, you have to evaluate them by increasing number. Evaluating them can involve several operations where you have to wait for each of them.
"Mutual exclusion" doesn't mean one thing happens at a time -- it means none of your code gets interleaved. If for one of your requests, you've got to evaluate a(); b(func { c(); });, and you want the effects of a, b, and c to happen atomically relative to the evaluation of other requests, you will have to explicitly manage that.
> There is no such thing as waiting for callbacks in event loops.
"Make that callback get called without waiting [to call it]" means calling it directly, instead of having it be called "later" as with setImmediate in Node.js for example. As you could have inferred if you chose the meaning that led to a synchronization problem, which would be the natural way to read things.
> No, you can't. Because of the way blocking code is written there is no guaranteed state of mutual exclusivity of everything where your signal/message handler can execute and operate in the same scope, without causing havoc.
Yes, you can. And what you say is prima facie nonsense. People have done it, it's not particularly interesting either. And believe it or not, there are ways to create a guaranteed state of mutual exclusivity in blocking threads, not that you necessarily need such a thing in the first place.
> If for one of your requests, you've got to evaluate a(); b(func { c(); });, and you want the effects of a, b, and c to happen atomically relative to the evaluation of other requests, you will have to explicitly manage that.
Well, yeah, you define relationships between them explicitly, by nesting event handlers if you want one to happen after the other one. It's just a matter of a habit. You'll get used to it. It's not that different from placing one statement after the other one in an imperative language.
> means calling it directly
Ok, I understand what you mean. It breaks the whole model though. Instead of calling it directly, callback must be "posted" to be executed by event loop.
Although I admit, I made that mistake myself in the past. Luckily, event driven code is very predictable and unit tests were able to caught it.
That's a big list of features, and essentially all of them, or the general ambient complexity that you're handwaving, have been hit by software written in a threaded style. And it wasn't a hindrance. Quite the opposite.
Having things be designed as event-driven may provide more opportunity for something incrementally more akin to proof of correctness. Add a large quantum of salt and caveat that; it's simply slightly easier that way.
People don't like it in general, but event driven works for me, mostly. Your mileage will almost certainly vary.
This works, until you start coding an imperative-style interpreter in the functional language, and mess everything up again :)
Anyway, I believe we should have the best of both worlds: a language which for the outer layers (the "outer loops" so to speak) uses functional style, and for the inner parts allows imperative style (of course the programmer should make sure that no side-effects remain visible here to the encompassing layer).
> The ST monad lets you use update-in-place, but is escapable (unlike IO). ST actions have the form:
ST s α
> Meaning that they return a value of type α, and execute in "thread" s. All reference types are tagged with the thread, so that actions can only affect references in their own "thread".
> Now, the type of the function used to escape ST is:
runST :: forall α. (forall s. ST s α) -> α
> The action you pass must be universal in s, so inside your action you don't know what thread, thus you cannot access any other threads, thus runST
is pure. This is very useful, since it allows you to implement externally pure things like in-place quicksort, and present them as pure functions ∀ e. Ord e ⇒ Array e → Array e; without using any unsafe functions.
> threads should only be used to exercise multiple CPU cores and never as a way to work around blocking IO.
Well, nothing, except the simplest of tasks, is pure IO. If you are talking about asynchronous programming, that is basically the cooperative multitasking of the Windows 3.1 era. One event handler will, if it performs a lot of computations, block another from running. Most people seem to forget that the CPU is a resource that can be blocked too. So in the end you still end up spawning threads to reduce latency.
You may be right that a lot of software has some CPU bounds parts but I would argue that most code is IO bound. Usually there is a only a small kernel in a program that actually does heavy computation and this is the only place you should use threading.
What's wrong with threading on IO? For example if you have a webserver which has to wait on replies from various other resources while serving enough clients to saturate itself.
Sure, performance wise it is probably better to have a fixed number of threads which do their own scheduling in userspace but this requires much more work then a simple if(!finished()){yield();} and making a thread per client.
Great advice, in general. One small point to remember is that it is impossible to implement a completely non-blocking output using the C++ ostream classes.
The underlying streambuf doesn't provide intrinsics to measure the remaining capacity of the output buffer, and may be forced to flush at any output request -- causing unavoidable blocking.
Of course, it is possible to implement your own streambuf class which provides an interface to measure remaining output buffering capacity. Just something to know...
C++ remains my favourite language; after a couple decades of intensive development using it, pretty much every other language always leaves me feeling "crippled", somehow.
While I admire the amount of thinking that went and still goes into C++, for me the question is opposite: why ever C++? It seems to me that trying to improve a language by ecosystem or convention will always be worse than using a language already designed around what we have learned and what has changed since C++ was born.
If somebody really needs to have the performace or be that close to the hardware, they can still probably find a better language today (Rust, Go?). But all others will certainly be better served by more modern, higher level languages. Just reading the examples in the PDF makes my head hurt.
> they can still probably find a better language today
Rust's ecosystem is still a bit anemic. I've played around with Rust, and I like the language, but C++ has a much wider array of battle-tested libraries at one's beck and call. Rust's development tools are also somewhat of a monoculture, at the moment.
For us we support multiple platforms (win, osx, ios, android, ...) and need to interact with hardware io, codecs, and gpu apis. We could use C, I suppose, but we'd be giving up some of the niceties of C++11 such as smart pointers that make life much simpler.
That's not to say we don't have some platform-specific code in the "preferred" language (in some cases there is no choice in the matter), but most of the system is C++.
In my limited experience, unfortunately Rust isn't there yet for true "blazingly-fast" low-level code, not without using assembly[1] or linking to external code. For example, there is no (mature) support for SIMD yet. Neither C or C++ has it to be technicall, but at least the compilers do (in the form of intrinsics).
But I agree with the overal message, and I'm looking forward to Rust maturing. I think it is the better language although I am no expert at either.
[1]: but really, when you start using AVX instructions and the like beyond what the compiler will automate for you, you are just using wrappers around assembly instructions in languages like C today anyway (__mm256_shuffpd, __mm256_perm128f2d)
Writing in other languages means you end up using ineffecient abstractions. C++ affords very efficient abstractions and really doesn't force a different code structure than other languages, so it is hard to see why there'd be an advantage with something else.
My main argument against Rust is the existence of D.
Over the years several languages have been the 'C++ killer'. For quite a long time it was D, and I know several people who converted. Then the standard library split in two, and the language got fairly unusable for a while. Now things have calmed down with D 2.0, but it's not clear it will still be around in 10-15 years time.
Similarly with Rust -- at the moment it seems to be a one company product (Mozilla). What if they get bored of it? Will my Rust code still work in 10 years time?
I asked about this and some Rust folks were kind enough to mention that Dropbox is one company that publicly has a large Rust deployment. Further, I think the main (huge?) advantage of Rust is it was designed to allow in situ replacements of pieces of a large c++ codebase. This really is a killer feature for lots of folks who simply can't afford to rewrite tens of millions of lines of tested c++.
Summarising how I usually seeing these discussions go:
The usual argument goes that C++ is bad because of the problems of C.
The counter-argument is that "nobody does that in modern C++".
The counter-counter argument is that:
1. there's a lot more not-modern-C++ than there is modern C++.
2. there seem to be dozens of not-really-smoothly-interchangeable notions of what "modern C++" is, because the language is so vast, and there have in fact been several overlapping generations of "modern C++".
3. It still requires conscious and vigilant effort, over and above the baseline the language gives you, to not introduce game-over security or reliability flaws.
The counter-counter-counter argument is that these are true of every language.
Which, being not strictly and completely true in the particulars, is where the whole thing degenerates into personal reflections about $PET_LANGUAGE, hair-pulling, personal insults, blog posts about monads at twenty paces etc etc.
> 2. there seem to be dozens of not-really-smoothly-interchangeable notions of what "modern C++" is, because the language is so vast, and there have in fact been several overlapping generations of "modern C++".
Everyone I've talked to understands modern C++ to be C++11 or newer.
C++11 adds to the language, but is there a strong consensus on what parts of pre-2011 C++ are not allowed in "Modern C++"? I have seen C++ code advertised as "modern" that still uses naked pointers, for example, while others claim that all pointers should be reference-counted. There is no agreed-upon algorithm that answers the question "is this source file written in valid Modern C++?", because there's no agreement on what that means.
> 2. there seem to be dozens of not-really-smoothly-interchangeable notions of what "modern C++" is, because the language is so vast, and there have in fact been several overlapping generations of "modern C++".
Everyone I've talked to understands modern C++ to be C++11 or newer.
> The counter-counter-counter argument is that these are true of every language.
I think the problem is that people keep treating these as binary attributes in some cases (usually the ones counter to their argument), and gradient ones in other cases, and this doesn't lead to effective communication. Almost all the attributes are gradient. The question is not whether C++ takes conscious and vigilant effort to not use a footgun, it's how much conscious and vigilant effort it takes in in comparison to an alternative, and for an alternative, it's not just that it may be slower, but how much slower.
I think that line of arguing misses an important point.
"modern" C++ is the best language we have that holds backwards compatibility with C and old C++ and the humongeous codebase that comes with that.
For many uses that backwards compatibility is of key importance. Therefore C++ does not really have to compete with other languages that don't have that kind of backwards compatibility.
If you want to sort languages into "good" and "bad" you first have to define the use case. Most languages are best at their own respective niches. To cover a large ground, in large project C++ is often combined with scripting languages like Python and Lua to get the best of both worlds.
C++ is still plenty viable, but if you like avoiding bugs, John Carmack seems to have come to the conclusion that using a functional style (which is not exactly encouraged by C++) is advantageous: http://gamasutra.com/view/news/169296/Indepth_Functional_pro...
"My pragmatic summary: A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in. In a multithreaded environment, the lack of understanding and the resulting problems are greatly amplified, almost to the point of panic if you are paying attention. Programming in a functional style makes the state presented to your code explicit, which makes it much easier to reason about, and, in a completely pure system, makes thread race conditions impossible.
"No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient."
I clicked the title and it provided a great answer. something about mysql not connecting. i thought it answered the question until i googled it. a better title would be 'why not php'.
The only thing that has always been behind, and might still be for a couple of years, is the include system, which causes huge delays when compiling and linking, even when editing one single file.
When I make edits and hit build, the old C include system kicks in and it's a very long process. I'm using C++ but precompiled headers are not a standard (many details will prevent you to use them) and there are not decent ways to reduce build time to bearable ranges.
I'm sure this is the main point that makes C++ so unattractive. If you're a programmer, having shorter code-write/build/test/repeat cycles is important if you don't want to lose focus on what you're doing. I don't know if build delays are specific to C++ or specific to microsoft, but if modules were available in some beta form I would try them ASAP. Even platform specific techniques would interest me...
So yes, I'll love C++, but not only the ABI and build configs are not easily solved by CMake, but build times will never make C++ attractive enough. It's not not realistic to pretend you can be really productive with C++.
Working on a c++ project: I feel stupid... and nothing works.
Working on a golang project: go is stupid... but everything works.
My issue with c++ is someone needs to write a "c++: the good parts" I may end up with rust, but right now I look at the documentation and some rust code and it's a bit overwhelming.
Go may be stupid in a lot of ways, but for the most part it's pretty obvious how to accomplish something.
Trust me even though it's not updated everything he mentions in the book is still valid. If you want to really know more about specifically modern C++ there is always Modern Effective C++
I think Scott Meyers' "Effective C++" may be the "good parts" book you're looking for. As well as telling you how you should write "good" C++ it's also pretty good at explaining when you should break these rules too.
88 comments
[ 4.9 ms ] story [ 148 ms ] threadThat later point is a little more complicated/subtle and took me years of threaded programming to learn and accept. When adhering to these two rules, C++ is safe and extremely powerful.
or am i still not getting this?
2.a. is about performance. And while not specifically about C++ this article covers the main idea http://www.kegel.com/c10k.html
2.b. is about avoiding deadlocks and race conditions. Threaded programming in C++ is dangerous. For years I would have said, that I could avoid deadlocks and race conditions by virtue of being a good programmer. However, I've debugged enough of my own code to know this is not always true.
If you only use threads for data driven computation, then you greatly reduce the risks. On the other hand if you have multiple threads running around accessing anything and everything in your program then you need a lot of thread locks which is tedious to implement and not well supported in C++ (vs. say the synchronized keyword in Java), does not perform well and still it is very easy to make mistakes. The point is that there is no reason to let threads run willy nilly because you can do event driven programming in C++ (libev, libevent, libuv) and most programs (or rather most parts of a program) are not computationally intensive.
edit: Here's a great resource on the topic of threading. From the SQlite FAQ "Threads are evil. Avoid them." http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1....
It's about Java, not C++, but it's still relevant.
However, C++ really isn't that hard to learn. Harder to learn than some alternatives, perhaps, but only like driving a stick shift is harder than an AT, but neither really qualifies as simply hard.
Nginx vs. Apache is a great example of this difference. https://www.nginx.com/blog/inside-nginx-how-we-designed-for-...
As for stack allocation I definitely see your point there. It is bizarre that Linux defaults to 2MB or whatever it is these days. I usually work with a 64KB thread stack.
On the contrary, it gives your code clarity, consistency and predictability.
> some master continuation that will delete them all in the final callback
Even nginx in plain C is not doing that, but simply creates destructors on allocation to avoid tracking stuff manually.
Just don't jump into the first thing you read somewhere, remember one simple rule: if it feels harder than threads - you are doing it wrong.
Well, when a product I worked on gradually migrated from event loops with callbacks to threads, clarity, consistency, and predictability made big improvements. Some callback usage would have been better with C++11 or C++14, but it's still way worse than what you can do simply by having variables go in and out of scope on a stack.
It fails miserably once you have to deal with zero downtime restarts for continuous deployment, rate limiting, requests cancellations, waiting for one or multiple responses from multiple nodes, making more requests to other nodes just after a bit of time to guarantee latency, using a lot of different kinds of shared data, like response queues, log queues, connection caches, data caches, all used by multiple clients, while at the same time doing some synchronization of data between nodes, etc.
Anything non-trivial is impossible with threads in practice.
As for your specific points, I disagree totally. Each of those things is easier to me in the synchronous style than in the async style. Certainly something like hedging is way easier.
Also, if you use an event loop, you have to do scheduling yourself. For distributed key/value stores, good thread scheduling is even more important. It removes a big amount of technical risk to lean on the kernel's, unless your product is sufficiently minimalistic.
Async will get you real performance advantages for pure networking, but if you're talking to disk it's, when not buggy, not very useful anyway.
Also, as for programming style, even if you want event-based syscalls, cooperative userland-managed threads are a big win over callback hell. The only bad thing about them is wasting address-space on 32-bit systems, and maybe just wasting memory too.
Which is not the case with event-driven approach. Event-driven code forces you to write asynchronous functions differently from the rest. There is no confusion of what yields where and returns from where. There are just continuations. So, if foo() is asynchronous in the example above, you wouldn't modify global arr_index across callbacks, you either store its value or modify it in a single callback, because you can rely on callbacks to never get interrupted:
Furthermore, because of the very specific constraints on callbacks, event-driven approach allows to implement some sort of composable virtual processes, capable of cleaning up and keeping track of various resources and accepting signals from other virtual processes via callback. And it's pretty easy to do, so some event-driven applications in the wild do that, although not always call them processes.So, again, if you find event-driven programming in any way harder, than multithreaded - you are definitely doing it wrong. Because it completely frees you from thinking about synchronization without adding anything extra that you cannot get used to.
As for disks: there is more than one and you are not talking to them directly, there is a filesystem cache in between. Event loops typically use thread pools to fake asynchronous filesystem access and for other work. And it makes some sense latency and performance-wise, because request that waits for some disk does not block requests that want to access other disks, or hit cache, or maybe it's an ssd and accessing it in parallel is faster.
They introduce (ostensibly or in reality) performance, of the kind that obviates a mass of arguments in favor of event loops.
> There is no protection from this, synchronization primitives are still needed, just like with threads.
And it's not a problem.
> There is no confusion of what yields where and returns from where.
This isn't a problem in practice, except when you're (without due care, but it's easy to screw up) migrating from the event-based code that was written with this assumption. You write your code as if it could block anywhere, and then stop wondering whether something could block. If some code is totally not going to block, you use a mutex anyway, hopefully the asserting kind that logs a message if it ever has to wait.
It's easy for code written with callbacks to wind up with baked in, undocumented dependencies of chunks of code that aren't allowed to be split up is far worse to maintain than that which lays out that information explicitly.
Your example with arr_index is a great demonstration of something trivially solvable by protecting the shared field with a mutex. And if you did that, you would easily notice if foo() or bar() ever decided it should increment arr_index.
> Because it completely frees you from thinking about synchronization
Well this is untrue. So completely untrue!
(Edit: Hell, you very nearly showed it in your own examples. Make the first foo() run your foo(func...) example, make that callback get called without waiting in some cases, and bam, you've got a synchronization problem.
Callbacks do nothing to protect you against the class of synchronization problems where the order in which you do stuff matters. For example, naively written rate-limiting code using callbacks will allow incoming actions to pass through out-of-order and might also starve operations that had been blocked. Likewise, you are unprotected from that variable getting incremented when a callback gets called sooner than you'd expect -- and no, on some supposedly high-performance server under load, throwing every callback around the event loop an extra time is not an acceptable option.
Ultimately it's moot -- for sane enforcement of proper ordering, you have to rely on some sort of explicit mutual exclusion.)
> some sort of composable virtual processes, capable of cleaning up and keeping track of various resources and accepting signals from other virtual processes via callback.
And what if I told you that you can keep track of resources and accept signals from, well, anything, in cooperative (or non-cooperative) threads? Because, you can.
> As for disks: there is more than one and you are not talking to them directly, there is a filesystem cache in between.
Sometimes you are talking to them directly. Event loops use thread pools for disk I/O (instead of the event-based APIs on some platforms) because, well, I don't know what other peoples' motivations were, but I can tell you it's more portable and kernels seem less buggy when you do it that way.
Oh, see, we got somewhere. You seem to presume that it's ok to implement an event loop that doesn't guarantee mutual exclusivity of callbacks. But that's not the case, the whole point of event-driven programming is to guaranty that exclusivity, so you wouldn't have to think about synchronization. So, your arguments do not apply here at all.
EDIT:
> example, make that callback get called without waiting in some cases, and bam, you've got a synchronization problem
There is no such thing as waiting for callbacks in event loops. There is giving up control to a callback and getting it back once it returns. Or it's not an event loop otherwise.
> And what if I told you that you can keep track of resources and accept signals from, well, anything, in cooperative (or non-cooperative) threads? Because, you can.
No, you can't. Because of the way blocking code is written there is no guaranteed state of mutual exclusivity of everything where your signal/message handler can execute and operate in the same scope, without causing havoc.
No, that's not what I said at all. How could you even infer that? What I was talking about was, try imagining how you'd enforce a certain ordering of behaviors. For example, a series of requests come in, they're numbered, you have to evaluate them by increasing number. Evaluating them can involve several operations where you have to wait for each of them.
"Mutual exclusion" doesn't mean one thing happens at a time -- it means none of your code gets interleaved. If for one of your requests, you've got to evaluate a(); b(func { c(); });, and you want the effects of a, b, and c to happen atomically relative to the evaluation of other requests, you will have to explicitly manage that.
> There is no such thing as waiting for callbacks in event loops.
"Make that callback get called without waiting [to call it]" means calling it directly, instead of having it be called "later" as with setImmediate in Node.js for example. As you could have inferred if you chose the meaning that led to a synchronization problem, which would be the natural way to read things.
> No, you can't. Because of the way blocking code is written there is no guaranteed state of mutual exclusivity of everything where your signal/message handler can execute and operate in the same scope, without causing havoc.
Yes, you can. And what you say is prima facie nonsense. People have done it, it's not particularly interesting either. And believe it or not, there are ways to create a guaranteed state of mutual exclusivity in blocking threads, not that you necessarily need such a thing in the first place.
Well, yeah, you define relationships between them explicitly, by nesting event handlers if you want one to happen after the other one. It's just a matter of a habit. You'll get used to it. It's not that different from placing one statement after the other one in an imperative language.
> means calling it directly
Ok, I understand what you mean. It breaks the whole model though. Instead of calling it directly, callback must be "posted" to be executed by event loop.
Although I admit, I made that mistake myself in the past. Luckily, event driven code is very predictable and unit tests were able to caught it.
People don't like it in general, but event driven works for me, mostly. Your mileage will almost certainly vary.
Anyway, I believe we should have the best of both worlds: a language which for the outer layers (the "outer loops" so to speak) uses functional style, and for the inner parts allows imperative style (of course the programmer should make sure that no side-effects remain visible here to the encompassing layer).
https://wiki.haskell.org/Monad/ST
> The ST monad lets you use update-in-place, but is escapable (unlike IO). ST actions have the form:
> Meaning that they return a value of type α, and execute in "thread" s. All reference types are tagged with the thread, so that actions can only affect references in their own "thread".> Now, the type of the function used to escape ST is:
> The action you pass must be universal in s, so inside your action you don't know what thread, thus you cannot access any other threads, thus runST is pure. This is very useful, since it allows you to implement externally pure things like in-place quicksort, and present them as pure functions ∀ e. Ord e ⇒ Array e → Array e; without using any unsafe functions.Well, nothing, except the simplest of tasks, is pure IO. If you are talking about asynchronous programming, that is basically the cooperative multitasking of the Windows 3.1 era. One event handler will, if it performs a lot of computations, block another from running. Most people seem to forget that the CPU is a resource that can be blocked too. So in the end you still end up spawning threads to reduce latency.
Sure, performance wise it is probably better to have a fixed number of threads which do their own scheduling in userspace but this requires much more work then a simple if(!finished()){yield();} and making a thread per client.
To modify JWZ's famous joke:
You have a problem. You say, "I know, I'll use threads!"
Nowyou prob h2ave lems
The underlying streambuf doesn't provide intrinsics to measure the remaining capacity of the output buffer, and may be forced to flush at any output request -- causing unavoidable blocking.
Of course, it is possible to implement your own streambuf class which provides an interface to measure remaining output buffering capacity. Just something to know...
C++ remains my favourite language; after a couple decades of intensive development using it, pretty much every other language always leaves me feeling "crippled", somehow.
Never use threads in C++. Gotcha. Good thing I'm learning Rust, then.
If somebody really needs to have the performace or be that close to the hardware, they can still probably find a better language today (Rust, Go?). But all others will certainly be better served by more modern, higher level languages. Just reading the examples in the PDF makes my head hurt.
Rust's ecosystem is still a bit anemic. I've played around with Rust, and I like the language, but C++ has a much wider array of battle-tested libraries at one's beck and call. Rust's development tools are also somewhat of a monoculture, at the moment.
Care to elaborate? I'm curious
That's not to say we don't have some platform-specific code in the "preferred" language (in some cases there is no choice in the matter), but most of the system is C++.
Moore's law is coming to an end. It's only preferable that programmers use more efficient languages, even if happens at a mental cost.
It's also more environmentally friendly.
But I agree with the overal message, and I'm looking forward to Rust maturing. I think it is the better language although I am no expert at either.
[1]: but really, when you start using AVX instructions and the like beyond what the compiler will automate for you, you are just using wrappers around assembly instructions in languages like C today anyway (__mm256_shuffpd, __mm256_perm128f2d)
Writing in other languages means you end up using ineffecient abstractions. C++ affords very efficient abstractions and really doesn't force a different code structure than other languages, so it is hard to see why there'd be an advantage with something else.
Over the years several languages have been the 'C++ killer'. For quite a long time it was D, and I know several people who converted. Then the standard library split in two, and the language got fairly unusable for a while. Now things have calmed down with D 2.0, but it's not clear it will still be around in 10-15 years time.
Similarly with Rust -- at the moment it seems to be a one company product (Mozilla). What if they get bored of it? Will my Rust code still work in 10 years time?
previous discussion: https://news.ycombinator.com/item?id=10890310
The usual argument goes that C++ is bad because of the problems of C.
The counter-argument is that "nobody does that in modern C++".
The counter-counter argument is that:
1. there's a lot more not-modern-C++ than there is modern C++.
2. there seem to be dozens of not-really-smoothly-interchangeable notions of what "modern C++" is, because the language is so vast, and there have in fact been several overlapping generations of "modern C++".
3. It still requires conscious and vigilant effort, over and above the baseline the language gives you, to not introduce game-over security or reliability flaws.
The counter-counter-counter argument is that these are true of every language.
Which, being not strictly and completely true in the particulars, is where the whole thing degenerates into personal reflections about $PET_LANGUAGE, hair-pulling, personal insults, blog posts about monads at twenty paces etc etc.
Everyone I've talked to understands modern C++ to be C++11 or newer.
Everyone I've talked to understands modern C++ to be C++11 or newer.
I think the problem is that people keep treating these as binary attributes in some cases (usually the ones counter to their argument), and gradient ones in other cases, and this doesn't lead to effective communication. Almost all the attributes are gradient. The question is not whether C++ takes conscious and vigilant effort to not use a footgun, it's how much conscious and vigilant effort it takes in in comparison to an alternative, and for an alternative, it's not just that it may be slower, but how much slower.
"modern" C++ is the best language we have that holds backwards compatibility with C and old C++ and the humongeous codebase that comes with that.
For many uses that backwards compatibility is of key importance. Therefore C++ does not really have to compete with other languages that don't have that kind of backwards compatibility.
If you want to sort languages into "good" and "bad" you first have to define the use case. Most languages are best at their own respective niches. To cover a large ground, in large project C++ is often combined with scripting languages like Python and Lua to get the best of both worlds.
I'm not sure where this statement belongs. In a decision about which language is best suited for a project?
http://webcache.googleusercontent.com/search?q=cache:http://...
The circle of life :)
"My pragmatic summary: A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in. In a multithreaded environment, the lack of understanding and the resulting problems are greatly amplified, almost to the point of panic if you are paying attention. Programming in a functional style makes the state presented to your code explicit, which makes it much easier to reason about, and, in a completely pure system, makes thread race conditions impossible.
"No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient."
When I make edits and hit build, the old C include system kicks in and it's a very long process. I'm using C++ but precompiled headers are not a standard (many details will prevent you to use them) and there are not decent ways to reduce build time to bearable ranges.
I'm sure this is the main point that makes C++ so unattractive. If you're a programmer, having shorter code-write/build/test/repeat cycles is important if you don't want to lose focus on what you're doing. I don't know if build delays are specific to C++ or specific to microsoft, but if modules were available in some beta form I would try them ASAP. Even platform specific techniques would interest me...
So yes, I'll love C++, but not only the ABI and build configs are not easily solved by CMake, but build times will never make C++ attractive enough. It's not not realistic to pretend you can be really productive with C++.
Working on a c++ project: I feel stupid... and nothing works.
Working on a golang project: go is stupid... but everything works.
My issue with c++ is someone needs to write a "c++: the good parts" I may end up with rust, but right now I look at the documentation and some rust code and it's a bit overwhelming.
Go may be stupid in a lot of ways, but for the most part it's pretty obvious how to accomplish something.
I owe a lot to the books though.
https://github.com/isocpp/CppCoreGuidelines
As a working programmer here are some things that I consider to be C++ virtues
a. Control over memory layout
b. Control over allocation policies
c. Deterministic deallocation of objects on scope exit
d. Generics and specialization of generics
I am always mystified how many languages miss out on item a.