views:

314

answers:

7

There is a lot of buzz these days about not using locks and using Message passing approaches like Erlang. Or about using immutable datastructures like in Functional programming vs. C++/Java.

But what I am concerned with is the following:

  1. AFAIK, Erlang does not guarantee Message delivery. Messages might be lost. Won't the algorithm and code bloat and be complicated again if you have to worry about loss of messages? Whatever distributed algorithm you use must not depend on guaranteed delivery of messages.
  2. What if the Message is a complicated object? Isn't there a huge performance penalty in copying and sending the messages vs. say keeping it in a shared location (like a DB that both processes can access)?
  3. Can you really totally do away with shared states? I don't think so. For e.g. in a DB, you have to access and modify the same record. You cannot use message passing there. You need to have locking or assume Optimistic concurrency control mechanisms and then do rollbacks on errors. How does Mnesia work?
  4. Also, it is not the case that you always need to worry about concurrency. Any project will also have a large piece of code that doesn't have to do anything with concurrency or transactions at all (but they do have performance and speed as a concern). A lot of these algorithms depend on shared states (that's why pass-by-reference or pointers are so useful).

Given this fact, writing programs in Erlang etc is a pain because you are prevented from doing any of these things. May be, it makes programs robust, but for things like Solving a Linear Programming problem or Computing the convex hulll etc. performance is more important and forcing immutability etc. on the algorithm when it has nothing to do with Concurrency/Transactions is a poor decision. Isn't it?

+4  A: 

Real-world systems are always hybrids anyway: I don't believe the modern paradigms try, in practice, to get rid of mutable data and shared state.

The objective, however, is not to need concurrent access to this shared state. Programs can be divided into the concurrent and the sequential, and use message-passing and the new paradigms for the concurrent parts.

Not every code will get the same investment: There is concern that threads are fundamentally "considered harmful". Something like Apache may need traditional concurrent threads and a key piece of technology like that may be carefully refined over a period of years so it can blast away with fully concurrent shared state. Operating system kernels are another example where "solve the problem no matter how expensive it is" may make sense.

There is no benefit to fast-but-broken: But for new code, or code that doesn't get so much attention, it may be the case that it simply isn't thread-safe, and it will not handle true concurrency, and so the relative "efficiency" is irrelevant. One way works, and one way doesn't.

Don't forget testability: Also, what value can you place on testing? Thread-based shared-memory concurrency is simply not testable. Message-passing concurrency is. So now you have the situation where you can test one paradigm but not the other. So, what is the value in knowing that the code has been tested? The danger in not even knowing if the other code will work in every situation?

DigitalRoss
+5  A: 
  1. That's real life : you need to account for this possibility regardless of the language / platform. In a distributed world (the real world), things fail: live with it.

  2. Of course there is a cost: nothing is free in our universe. But shouldn't you use another medium (e.g. file, db) instead of shuttling "big objects" in communication pipes? You can always use "message" to refer to "big objects" stored somewhere.

  3. Of course not: the idea behind functional programming / Erlang OTP is to "isolate" as much as possible the areas were "shared state" is manipulated. Futhermore, having clearly marked places where shared state is mutated helps testability & traceability.

  4. I believe you are missing the point: there is no such thing as a silver bullet. If your application cannot be successfully built using Erlang then don't do it. You can always some other part of the overall system in another fashion i.e. use a different language / platform. Erlang is no different from another language in this respect: use the right tool for the right job.

Remember: Erlang was designed to help solve concurrent, asynchronous and distributed problems. It isn't optimized for working efficiently on a shared block of memory for example... unless you count interfacing with nif functions working on shared blocks part of the game :-)

jldupont
I guess then, the question I should be asking for my Q1 earlier is this: Which distributed algorithms are easier to design? Those relying on Shared state? Or those relying on Message passing. I believe for the same goal, the algorithms based on these 2 different approaches will require us to design totally different algorithms.Further, if (1) do Message passing and (2) don't use locking when avoidable is the rule, then can't we not do with in C++/Java too? Why need erlang for that?
ajay
I am confused here: if the system requirement calls for algorithm X then the question is: " Can X be supported by a distributed version variant of X, say D(X) ".
jldupont
jldupont
Can X be supported by a distributed version variant of X, say D(X)? Well yes, sort of. Let's say I want to do distributed leader election among a set of nodes. In this case, designing a distributed algorithm that elect a leader by using messages require more clever thinking than as compared to doing it with a shared record in a DB, lets say.
ajay
@Continued . On top of that, I also have to make the distributed message passing algorithm robust to loss of messages. With shared state, handling failed SQL updates is much easier to handle. Isn't it? Or you think it is just a matter of experience?
ajay
jldupont
K. Sure ........
ajay
+1  A: 

For e.g. in a DB, you have to access and modify the same record

But that is handled by the DB. As a user of the database, you simply execute your query, and the database ensures it is executed in isolation.

As for performance, one of the most important things about eliminating shared state is that it enables new optimizations. Shared state is not particularly efficient. You get cores fighting over the same cache lines, and data has to be written through to memory where it could otherwise stay in a register or in CPU cache.

Many compiler optimizations rely on absence of side effects and shared state as well.

You could say that a stricter language guaranteeing these things requires more optimizations to be performant than something like C, but it also makes these optimizations much much easier for the compiler to implement.

Many concerns similar to concurrency issues arise in singlethreaded code. Modern CPUs are pipelined, execute instructions out of order, and can run 3-4 of them per cycle. So even in a single-threaded program, it is vital that the compiler and CPU is able to determine which instructions can be interleaved and executed in parallel.

jalf
A: 

For correctness, shared is the way to go, and keep the data as normalized as possible. For immediacy, send messages to inform of changes, but always back them up with polling. Messages get dropped, duplicated, re-ordered, delayed - don't rely on them.

If speed is what you're worried about, first do it single-thread and tune the daylights out of it. Then if you've got multiple cores and know how to split up the work, use parallelism.

Mike Dunlavey
I'm working on a project that someone implemented single threaded and tuned the daylights out of a few years ago. Now we want to port it to CUDA where we may have several thousand threads at one time. It was impossible to do a direct port, we had to rewrite from the ground up because the 'tuning' had made the code completely unusable. I don't think this is a good approach to any kind of concurrent problem.
Andrew Myers
@Andrew: Sadly, tuning can be done badly. I don't believe tuning has to make code unmaintainable or otherwise evil. I still think that there's no good reason any given thread should be doing things that aren't truly necessary. And I've seen too much code that placed *primary* reliance on message-passing, and then was never fully problem-free, due to the difficulty of assuring that the messaging architecture was completely reliable.
Mike Dunlavey
I agree that tuning can be done badly, I'm not the one who downvoted you because there are times when tuning is the right way to go. I just think that if you're really performance constrained then parallelism should come first if your platform makes that reasonable. Admittedly that's my personal bias, I prefer concurrent code that's as clean as possible to serial code that's been tuned a lot. I also think that it's an easier way to see a large speedup if your algorithm can be parallelized well, but that's a problem specific consideration and in some cases tuning may be the only way to go.
Andrew Myers
@Andrew: Sounds like common sense to me. I don't see it as either-or, and I'm sure you don't either. It's just that, if the single-thread code is at all complicated, often it can be sped up by factors that are hard to believe, like 40 (http://stackoverflow.com/questions/926266/performance-optimization-strategies-of-last-resort/927773#927773), without particularly mangling it. Then if you can get another factor by parallelism, so much the better.
Mike Dunlavey
+2  A: 

A few comments on the misunderstanding you have of Erlang:

  • Erlang guarantees that messages will not be lost, and that they will arrive in the order sent. A basic error situation is that machine A can not speak to machine B. When that happens process monitors and links will trigger, and system node-down messages will be sent to the processes that registered for it. Nothing will be silently dropped. Processes will "crash" and supervisors (if any) tries to restart them.
  • Objects can not be mutated, so they are always copied. One way to secure immutability is by copying values to other erlang process' heaps. Another way is to allocate objects in a shared heap, message references to them and simply not have any operations that mutate them. Erlang does the first for performance! Realtime suffers if you need to stop all processes to garbage collect a shared heap. Ask Java.
  • There is shared state in Erlang. Erlang is not proud of it, but it is pragmatic about it. One example is the local process registry which is a global map that maps a name to a process so that system processes can be restarted and claim their old name. Erlang just tries to avoid shared state if it possibly can. ETS tables that are public are another example.
  • Yes, sometimes Erlang is too slow. This happens all languages. Sometimes Java is too slow. Sometimes C++ is too slow. Just because a tight loop in a game had to drop down to assembly to kick off some serious SIMD-based vector mathematics you can't deduce that everything should be written in assembly because it is the only language that is fast when it matters. What matters is being able to write systems that have good performance, and Erlang manages quite well. See benchmarks on yaws or rabbitmq.

Your facts are not facts about Erlang. Even if you think Erlang programming is a pain, you will find other people create some awesome software thanks to it. You should attempt writing an IRC server in Erlang, or something else very concurrent. Even if you're never going to use Erlang again, you would have learned to think about concurrency another way. But of course, you will, because Erlang is awesome easy.

Those that do not understand Erlang are doomed to re-implement it badly.

Okay, the original was about Lisp, but... its true!

Christian
A: 
  1. Erlang provides supervisors and gen_server callbacks for synchronous calls, so you will know about it if a message isn't delivered: either the gen_server call returns a timeout, or your whole node will be brought down and up if the supervisor is triggered.
  2. usually if the processes are on the same node, message-passing languages optimise away the data copying, so it's almost like shared memory, except if the object is changed used by both afterward, which can not be done using shared memory either anyways
  3. There is some state which is kept by processes by passing it around to themselves in the recursive tail-calls, also some state can be of course passed through messages. I don't use mnesia much, but it is a transactional database, so once you have passed the operation to mnesia (and it has returned) you are pretty much guaranteed it will go through..
  4. Which is why it is easy to tie such applications into erlang with the use of ports or drivers. The easiest are the ports, it's much like a unix pipe, though I think performance isn't that great...and as said, message-passing usually ends up just being pointer passing anyways as the VM/compiler optimise the memory copy out.
glenda
+2  A: 

There are some implicit assumption in your questions - you assume that all the data can fit on one machine and that the application is intrinsically localised to one place.

What happens if the application is so large it cannot fit on one machine? What happens if the application outgrows one machine?

You don't want to have one way to program an application if it fits on one machine and a completely different way of programming it as soon as it outgrows one machine.

What happens if you want make a fault-tolerant application? To make something fault-tolerant you need at least two physically separated machines and no sharing. When you talk about sharing and data bases you omit to mention that things like mySQL cluster achieve fault-tolerence precisely by maintaining synchronised copies of the data in physically separated machines - there is a lot of message passing and copying that you don't see on the surface - Erlang just exposes this.

The way you program should not suddenly change to accommodate fault-tolerance and scalability.

Erlang was designed primarily for building fault-tolerant applications.

Shared data on a multi-core has it's own set of problems - when you access shared data you need to acquire a lock - if you use a global lock (the easiest approach) you can end up stopping all the cores while you access the shared data. Shared data access on a multicore can be problematic due to caching problems, if the cores have local data caches then accessing "far away" data (in some other processors cache) can be very expensive.

Many problems are intrinsically distributed and the data is never available in one place at the same time so - these kind of problems fit well with the Erlang way of thinking.

In a distributed setting "guaranteeing message delivery" is impossible - the destination machine might have crashed. Erlang cannot thus guarantee message delivery - it takes a different approach - the system will tell you if it failed to deliver a message (but only if you have used the link mechanism) - then you can write you own custom error recovery.)

For pure number crunching Erlang is not appropriate - but in a hybrid system Erlang is good at managing how computations get distributed to available processors, so we see a lot of systems where Erlang manages the distribution and fault-tolerent aspects of the problem, but the problem itself is solved in a different language.

and other languages are used

ja
Thank you! Nicely summed it up
ajay