The sign says "You must be at least this tall to write threaded code", and it's been placed 8 feet off the floor, next to a well respected senior developer who must constantly write threaded code (and who, when standing, is a good two feet shy of the sign).
Threading properly is hard, and yet it's becoming more and more integrated into our lives, from our browser to our backend interpreters like Node...
Concurrency needs a Rust-like language to protect us at compile time against data races. The restrictions it would impose on what we could do might be abhorrent to our generation of coders, but the benefits would be far reaching.
Race conditions can and do occur with message passing as well. All that is required is to depend on operations split between threads (processes, machines) occurring in a particular order, and you will eventually run into a race condition when you try and run the code concurrently.
The problem occurs in that both programmers and our tools are terrible at guessing when such order dependencies will occur, except when working with "embarrassingly parallel" tasks.
But this isn't a "data race", which is what the article is about. Reasoning about data races is different than reasoning about what happens when events happen out of order. I would say the former is harder in general.
The default should be message passing concurrency, as Go advocates -- which of course can be done with simple threads and queues. Unfortunately in C++ that leads to tricky memory management issues.
You are correct - not all race conditions are data races, and message passing/immutable shared data will help prevent traditional shared memory problems.
That said, you can still have a data race with "immutable" data at the thread level as well.
For example, we have a system which has A processing threads, whose results are collated by the master and passed to a set of B processing threads. All of these threads use message passing and an immutable view of the master's environment which determines their behavior.
A command comes in which changes the master's environment, and the master sends the A and B threads a "die" command and waits for the flow of data out of the threads to stop. It then spins up new A threads and B threads to process the data with the new environment.
The data race appears when some old A threads become blocked on some external resource or are just unlucky in scheduling, passing data based off of outdated assumptions to the new B processors will be attempting to process data from the original A threads which have the wrong view of data.
The solution in this case is obvious - set up new queues, or more authoritatively kill off old threads, or change a parameter in the messages being passed, or pass the entire state from thread to thread, or... but the solutions to the problems outlined in the OP are also obvious, once the race condition is identified.
Figuring out the possible global combinations of multiple different interacting state machines can still be tricky. You can still easily get into racy scenarios.
The problems you see with memory races (e.g. publishing a pointer to a structure which does not have all its construction memory stores retired) are at a different level of strangeness, however.
While explicitly sharing memory isn't a great solution to threading, one shouldn't think message passing is some sort of panacea either, you can end up with things such as race conditions or deadlocks if you're not careful there as well.
Though it is admittedly a fair bit more sane for most applications.
I wish I remember who I first heard say "everyone agrees shared mutable state is evil. Most languages deal with this problem by controlling the 'mutable.' Rust deals with this by controlling the 'shared.'
Message passing is another way of dealing with the 'shared,' too. But especially with the new RFC that just landed, Rust has some pretty amazing capabilities in this space that should make it incredibly useful for concurrency. And because it's based in the type system, not the language itself, you can take advantage of that safety and write your own concurrency abstraction with the same guarantees, esentially for free.
It probably wasn't the first occurrance, but I have a vague memory of hearing it formulated like that first from Niko Matsakis in a Rust video on Air Mozilla.
> Concurrency needs a Rust-like language to protect us at compile time against data races. The restrictions it would impose on what we could do might be abhorrent to our generation of coders, but the benefits would be far reaching.
Not sure if this is what you meant, but Rust does protect against data races. You are unable to share data unless you use message passing, share read-only data, use atomics, or use mutexes.
Yes. Functional languages are amenable to composition. Because of their "purity". This allows trivial extraction of parallelism/concurrency. No need to reason about interleaving and shared data and such.
But, I don't know if "use FP if you want parallelism" is an acceptable solution currently. My intuition is that latency sensitive application can't use these languages because they don't expose the low-level primitives (read pointers) that make the difference in such applications. But I haven't written and profiled enough FP code to know if this is absolutely the case.
No, Functional languages reduce the problems surface area but they don't eliminate it. You can still get data races and deadlocks in Haskell. They aren't the same as a memory race in say C where the contents of a variable might change out from under you in truly bizarre ways but you still get algorithmic races where the wrong answer comes our the other end because two parallel operations happened in the wrong order.
It's possible to write race-prone code in Haskell if you have several threads reading and writing IORefs concurrently in the IO monad. There are plenty of safer alternatives, though, so there's not much point in writing software that way unless you're actually trying to create a race.
MVars can deadlock, but you can't accidentally mutate/read a value without taking the lock (unless you use IORefs in conjunction with MVars).
STM handles races by detecting them and re-starting a transaction if a race occurs, so races aren't possible. (Accessing IORefs is disallowed by the type system inside an STM transaction, so you're safe there.) STM also isn't susceptible to deadlocks, so it's generally the safest option if you need non-deterministic threading, and invoking transactions from the IO monad fits with the structure of your program.
There's also par/seq, strategies, and the Par monad which are great for parallel computation and aren't prone to races or deadlocks. They can be called from anywhere in a Haskell program and are deterministic -- they're guaranteed to always return the same result when given the same inputs. Results cannot depend on the order in which threads are scheduled.
30 comments
[ 2.6 ms ] story [ 66.5 ms ] threadThe sign says "You must be at least this tall to write threaded code", and it's been placed 8 feet off the floor, next to a well respected senior developer who must constantly write threaded code (and who, when standing, is a good two feet shy of the sign).
Threading properly is hard, and yet it's becoming more and more integrated into our lives, from our browser to our backend interpreters like Node...
Concurrency needs a Rust-like language to protect us at compile time against data races. The restrictions it would impose on what we could do might be abhorrent to our generation of coders, but the benefits would be far reaching.
The problem occurs in that both programmers and our tools are terrible at guessing when such order dependencies will occur, except when working with "embarrassingly parallel" tasks.
The default should be message passing concurrency, as Go advocates -- which of course can be done with simple threads and queues. Unfortunately in C++ that leads to tricky memory management issues.
That said, you can still have a data race with "immutable" data at the thread level as well.
For example, we have a system which has A processing threads, whose results are collated by the master and passed to a set of B processing threads. All of these threads use message passing and an immutable view of the master's environment which determines their behavior.
A command comes in which changes the master's environment, and the master sends the A and B threads a "die" command and waits for the flow of data out of the threads to stop. It then spins up new A threads and B threads to process the data with the new environment.
The data race appears when some old A threads become blocked on some external resource or are just unlucky in scheduling, passing data based off of outdated assumptions to the new B processors will be attempting to process data from the original A threads which have the wrong view of data.
The solution in this case is obvious - set up new queues, or more authoritatively kill off old threads, or change a parameter in the messages being passed, or pass the entire state from thread to thread, or... but the solutions to the problems outlined in the OP are also obvious, once the race condition is identified.
The problems you see with memory races (e.g. publishing a pointer to a structure which does not have all its construction memory stores retired) are at a different level of strangeness, however.
Though it is admittedly a fair bit more sane for most applications.
It still isn't "easy", but at least it's back in the realm of feasible.
Message passing is another way of dealing with the 'shared,' too. But especially with the new RFC that just landed, Rust has some pretty amazing capabilities in this space that should make it incredibly useful for concurrency. And because it's based in the type system, not the language itself, you can take advantage of that safety and write your own concurrency abstraction with the same guarantees, esentially for free.
Not sure if this is what you meant, but Rust does protect against data races. You are unable to share data unless you use message passing, share read-only data, use atomics, or use mutexes.
But, I don't know if "use FP if you want parallelism" is an acceptable solution currently. My intuition is that latency sensitive application can't use these languages because they don't expose the low-level primitives (read pointers) that make the difference in such applications. But I haven't written and profiled enough FP code to know if this is absolutely the case.
I thought separate functions weren't allowed to have dependencies like this?
Once your code is in a monad, every one of those problems may be back (including memory race).
https://ghc.haskell.org/trac/ghc/wiki/StaticPointers
STM is actually really simple in Haskell, and implemented mostly as a library(!). Have a look:
http://computationalthoughts.blogspot.nl/2008/03/some-exampl...
MVars can deadlock, but you can't accidentally mutate/read a value without taking the lock (unless you use IORefs in conjunction with MVars).
STM handles races by detecting them and re-starting a transaction if a race occurs, so races aren't possible. (Accessing IORefs is disallowed by the type system inside an STM transaction, so you're safe there.) STM also isn't susceptible to deadlocks, so it's generally the safest option if you need non-deterministic threading, and invoking transactions from the IO monad fits with the structure of your program.
There's also par/seq, strategies, and the Par monad which are great for parallel computation and aren't prone to races or deadlocks. They can be called from anywhere in a Haskell program and are deterministic -- they're guaranteed to always return the same result when given the same inputs. Results cannot depend on the order in which threads are scheduled.
tl;dr: don't use IORefs and you should be fine
http://www.gnu.org/software/libc/manual/html_node/Atomic-Dat...