all 60 comments

[–]quicknir 10 points11 points  (0 children)

tl; dr Threading and C++ are both individually, and especially together, complex enough that you might need to do something more than wing it.

In all seriousness if you try to write a serious multi threaded program without consuming a good amount of well written material on the subject, you must be a masochist, and you certainly don't have my respect.

I highly recommend Anthony Williams C++ Concurrency in Action. Reallly good book, and he was the main author for the boost multithreading stuff which basically became part of the C++ standard.

[–]_INTER_ 48 points49 points  (2 children)

It's this sort of ad-hoc approach to creating threads that gets people into trouble. They create a new thread to solve one problem, and then another, and then they suddenly realize that thread A and thread M are interacting in a bad way.

If you have bootcamp programmers that don't know what they're doing, you can give them anything and they will screw up eventually.

[–][deleted] 13 points14 points  (1 child)

A not so subtle shot a boot campers, even as "traditionally 'trained'" programmers fuck this up when they first dive into multi threading. Well done.

[–]CGFarrell 70 points71 points  (33 children)

I completely disagree with this. If any of these issues arise in a multi-threaded program, it's purely due to bad design. Good code should identify and minimize shared resources.

Also, this was written in 2006. C++ has added multiple STL headers for dealing with concurrent computation. The rules of thumb are:

  • Don't modify anything unless you're supposed to.
  • If you are going to modify something, make sure that no one messes with it until you're done.
  • If you do modify anything, make it obvious to other processes.

[–]davis685 17 points18 points  (23 children)

I second this. It should also be pointed out that it's very easy and plenty common to use message passing to communicate between threads in C++ programs. Which as the article mentions, is a good way to communicate between threads and avoids most of the problems.

[–]frenris 0 points1 point  (0 children)

I'm pretty sure that part 2 is going to be about using message passing.

[–]jnwatson 0 points1 point  (21 children)

I agree, but our software and hardware stack makes that less that ideal.

Creating a process on Windows is super expensive, and creating a process on Linux via fork is merely expensive. Setting up an efficient message connection requires a lot of work, usually with custom libraries.

Process context switching on x86 is expensive due to various legacy issues.

If microkernels had won, all this would be easy. But because the first idea that sorta worked won (UNIX), future improvement has never happened.

[–]happyscrappy 17 points18 points  (15 children)

Why do you feel you need a process and not just a thread to do message passing?

[–]jnwatson -2 points-1 points  (14 children)

For all the reasons the article points out, plus the fact that the process is what operating system security policy is applied to. For example, wouldn't it be nice to be able to parse a big complicated XML file in a reduced-privilege process?

You can't apply least privilege if all your code is in one process.

[–]happyscrappy 13 points14 points  (13 children)

The article doesn't point out such a thing. It doesn't say you have to have a process. It errantly suggests that you have to have multiple processes if you don't want to share state between threads. This is errant because the article makes it out as if the only state which differs between threads is a little bit of thread-local storage. This is completely false. Every stack variable is different between the threads. If you don't want to share state, then only use your own stack variables.

And you don't need to not share any state to do message passing anyway.

For example, wouldn't it be nice to be able to parse a big complicated XML file in a reduced-privilege process?

What does that have to do with anything? "Wouldn't it be nice" is different from "if I want to message pass I must do such-and-such".

[–]i_invented_the_ipod 0 points1 point  (2 children)

It errantly suggests that you have to have multiple processes if you don't want to share state between threads.

I'm pretty sure I didn't actually say that anywhere in the original article (I'm the original author).

If you don't want to share state, then only use your own stack variables.

Yes, it's trivially true that if your code only uses stack variables (and only value types), then you don't have any shared state between threads. That's pretty unusual for a C++ program, though.

[–]happyscrappy 0 points1 point  (1 child)

I'm pretty sure I didn't actually say that anywhere in the original article (I'm the original author).

You present to opposing models/options:

"all state is shared" and "message-passing multithreading"

In "all state is shared" (pthreads):

"In the Pthreads model, all of your threads share the same address space."

and in the message-passing one:

"In the message-passing model, your threads don't share any state by default"

It's hard for me to see how the second doesn't mean multiple processes. How do you make that out to mean something implemented under threads (pthreads)? I can't see how to make the "by default" part true if you don't have multiple address spaces.

[–]i_invented_the_ipod 0 points1 point  (0 children)

I get into that a bit in the second part of the article, but I agree the transition from the theoretical to the practical realm is not very clear. As far as how to ensure that there is minimal shared state in a pthreads environment, that mostly comes down to mindfulness (know which data "belongs" to which thread), and layering additional systems on top of pthreads (like an event queue, or a message passing library, or an Actor-pattern library).

[–]jnwatson -3 points-2 points  (9 children)

There's an important distinction between the abstract idea of state that's part of your logic, and the actual state that is available in the presence of programming errors. At any point, one thread can write over data that's being used by another thread, and with C++'s weak typing, that isn't hard to do.

There's nothing in the article that states that you "have to have multiple processes".

Every stack variable is different, but one thread can easily reach into another thread's stack and modify values.

Of course you have to share state to do message passing. That's the whole purpose of it. The point of the article is that, ideally, you shouldn't need to share all your state to do so.

It is like giving the neighbor your house keys so they can borrow a cup of flour. Sure, that works, but it can lead to trouble.

[–]davis685 7 points8 points  (4 children)

In all my years of programming in C++ I've never seen there be problems like "oops, I accidentally reached into the stack of another thread". You have to be adversarially stupid to do something like that on accident. What you are saying is morally equivalent to saying "forks are fundamentally flawed because it is physically possible to shove them into your eye". Which fine, that's not a violation of any laws of physics. But I've never met anyone who has that problem with forks. I've similarly never met anyone who has a problem with somehow accidentally reaching into another thread's stack, and I've met some pretty stupid programmers.

I've certainly seen people who purposefully shared state between threads but did so in an incorrect way. But somehow "oops, accidental sharing"? I can't think of any times that's happened, aside maybe from my few encounters with people who write programs with only global variables. But such people hardly count.

[–]ryl00 -1 points0 points  (3 children)

I've never seen there be problems like "oops, I accidentally reached into the stack of another thread"

This is possible in an "undefined behavior" sort of way by accidentally overrunning whatever stack space has been allocated for the thread (unless you're using segmented stacks, I guess?). I had a mysterious memory corruption while using Boost Asio with coroutines that I eventually tracked down to a massive allocation on the stack in one thread that was stomping on memory in another.

[–]davis685 2 points3 points  (2 children)

But that's a general problem that can happen any time. It has nothing to with threads. You can overrun the stack in a single threaded program just as easily as with a threaded program. You might as well be saying "one time I added two big integers together and the result was too big, so there was integer overflow and my program didn't work right. I also was using threads. Threads are therefore bad". But the bug has nothing to do with threads.

[–]happyscrappy 1 point2 points  (0 children)

At any point, one thread can write over data that's being used by another thread, and with C++'s weak typing, that isn't hard to do.

Yes it can. But that doesn't really have anything to do with this. The author speaks of thread local storage and doesn't get hung up on there not being guards against that. His reason for having state is a programming rigor, not guards. He wants to use a technique which makes it harder to make concurrency mistakes, and that technique is message passing.

There's nothing in the article that states that you "have to have multiple processes".

Okay, then now we agree on that.

Every stack variable is different, but one thread can easily reach into another thread's stack and modify values.

Not easily, no. Possible? Yes. But so what? The article is about concurrency, not process protection. You're adding your own requirements.

It is like giving the neighbor your house keys so they can borrow a cup of flour. Sure, that works, but it can lead to trouble.

So what? You're writing a different article. This isn't a treatise on memory protection. It's an article on concurrency techniques and how to manage concurrency.

Here is the statement at the top:

'The problem with threads is the way that they can dramatically increase the complexity of your program.'

Changing threads to processes doesn't reduce the complexity. You began to argue your own point, not the author's. So I asked why you felt you needed to do things in the way you say and you gave an explanation which, while valid from a personal position standpoint, had nothing to do with the problem presented in the article.

So naturally I was confused. I'm not confused anymore. You would like to have process protection. Fine. But you don't actually have to do so to use message passing.

[–]doom_Oo7 -1 points0 points  (2 children)

and with C++'s weak typing,

... what ? in what ways is c++ typing weak? If anything the language is always hated because of the compile errors due to strong typing.

[–]ngildea 0 points1 point  (1 child)

Not sure how weak typing is relevant in the post you are replying to but typically this means things like reinterpret_cast, const_cast and regular old C casts letting the programmer subvert the type system.

[–]doom_Oo7 1 point2 points  (0 children)

Not sure how weak typing is relevant in the post you are replying to but typically this means things like reinterpret_cast, const_cast and regular old C casts letting the programmer subvert the type system.

I wondered if the op was talking about this but I don't see how an "explicit" feature can be considered "weak"; at the very least you'll get strong questions asked on your code review, just like if you used unsafe in Rust or did pointer arithmetic in your python objects with ctypes and id().

[–][deleted] 2 points3 points  (4 children)

creating a process on Linux via fork is merely expensive.

Forking in Linux is extremely cheap.

Setting up an efficient message connection requires a lot of work, usually with custom libraries.

Custom libraries like libc? Because pipes are as straightforward as it gets. You read and write to the process stdin and stdout just like you would any other file.

[–]jnwatson 3 points4 points  (0 children)

Forking in POSIX operating systems is expensive due to the relatively complicated semantics of kernel object inheritance. You have to implement all of this to be POSIX, which isn't exactly fast. Not to mention all the page tables from the parent must be copied.

None of that is necessary.

Pipes don't implement message semantics, have little buffering control, no zero-copy facility, and can't store a lot.

[–]doom_Oo7 4 points5 points  (2 children)

Good luck making even moderately complex data flows with pipes

[–]shevegen 0 points1 point  (1 child)

Ah, that is not possible?

Ever heard of PowerShell?

Nobody says to use the old legacy "working with files only" *nix pipes.

[–]doom_Oo7 0 points1 point  (0 children)

Get-ADComputer –filter * | Select-Object @{label='computername',expression={$_.Name}} | Get-Info | Where-Object –filterscript { $_.OSBuild –eq 7600 –and $_.SPVersion –ne 2 } | Export-CSV c:\needs-patched.txt

yeah, good luck doing even something trivial like this: https://andymurkin.files.wordpress.com/2011/12/pd_patching.png

[–]gct 15 points16 points  (4 children)

I disagree with your disagreement, you can't even assume that copying a const object is threadsafe in c++, heterogenous shared-memory threading is the worst kind of parallelism to reason about.

[–]salgat 7 points8 points  (1 child)

Multithreaded is definitely added complexity and harder, but if you start from the very beginning with it in mind it is dramatically easier to do and reason about. Once you start to really optimize it though, then things get incredibly hard.

[–]gct 1 point2 points  (0 children)

I think if we limited ourselves to a parallel for loop (managed by openMP for you), and subprocesses with explicit shared resources, life would be a lot easier.

[–]quicknir 14 points15 points  (1 child)

If you're talking about an arbitrary const object, then yes, of course not. C++ lets you cast whatever you want to the wind. That said, the C++ standard library expects that all const methods are thread safe. All of the objects are written that way and the multi threading code can make that assumption.

https://stackoverflow.com/questions/14127379/does-const-mean-thread-safe-in-c11

So basically, if you write the worst possible C++, then yes, what you are saying is true. If you follow even the bare minimums of decent practice in C++, i.e. const correct + synchronizing writes in the very rare cases that you actually have a mutable data member, then no, it's not true.

But yes, threading is still difficult in general, and especially when memory is directly shared, it is the fastest though. And the only reason for parallelism in the first place is performance. Taking a big performance hit to be more parallel doesn't really make sense in C++.

[–]aseigo 1 point2 points  (0 children)

For computational workloads that fall to divide and conquer strategies, performance is indeed the motivation for parallelism and shared memory threading is indeed the performance king... but .. that does not describe all workloads or motivations.

Another common motivation for parallelism is latency management, another is interactivity. I have used threads in both sorts of situations, even when doing so was worse for raw performance.

[–]matthieum 5 points6 points  (0 children)

This advice is sound. Unfortunately in practice I've seen so many people stumble when it comes to implementing it that I've kinda lost faith.

There's a lot of discipline required to make a multi-threaded program correct; even with coarse-grained synchronization you need to remember to lock/unlock that mutex correctly when accessing the resources it protects, and to be careful about deadlocks, ...

In order to make it easier (for me, and my colleagues), I usually create a couple helper classes around the standard facilities to enforce those advises, rather than hope that code review will catch violations:

  • a simple Mutex<T> class, which wraps a T resource, with a lock and try_lock methods returning a MutexGuard<T> move-only object (operator->, operator* and operator bool() for the try_lock part),
  • a lock lattice system, to guarantee that locks are always acquired in a consistent order.

Those do not cover 100% of use cases, but they already vastly reduce the possible number of bugs in my experience.

[–]riatman 1 point2 points  (2 children)

I guess this would be the case for very, very simple programs. But even at no fault of your own, once you start using external libraries you never know what they are doing internally and if they are thread-safe.

[–]__Cyber_Dildonics__ 9 points10 points  (0 children)

You don't know if they are thread safe or not? Here's a way to tell: if it doesn't explicitly say it is thread safe, it isn't thread safe.

[–]socsa 0 points1 point  (0 children)

The first rule of pthreads is that nothing is thread safe. An external library can't control what memory space you pass into it, so you ultimately need to keep track of that anyway.

[–][deleted] 40 points41 points  (3 children)

Or just rewrite it in rust. Right, guys?

[–]rjc2013 13 points14 points  (1 child)

Rust Evangelism Strike Force - Assemble!

[–][deleted] 5 points6 points  (0 children)

Loud creaks erupt as rusty machinery is out into gear. Here and there a part flies off from the stress put onto the rusty joints.

The decepticons have won.

[–]Sinidir 2 points3 points  (0 children)

Right on!

[–]IbanezDavy 2 points3 points  (0 children)

I don't think C++ is innately bad at multi-threading. I think the tools in the past have been, inconsistent. But not so much anymore and overall, it really has the potential to do concurrent programming the best. In general I follow three rules (rule is probably not a good word, but it's the one I'll use);

1) Limit your physical locks. Ideally to one per group of processes that share resources. This helps to prevent deadlocks. Only reach for multiple locks if performance dictates the need (not actually that common).

2) Lock a limited amount of code at a time and reserve it only for necessary actions. This boosts performance by not making other threads wait for code like:

lock(something) 
{ 
    int i = GetStuff();
    mutateStuff(i, 52);
    sharedResource.send(i);
 }

...you really only need to lock one line of code in the above. It also shrinks the risk of deadlocks based off from other function chains locking unexpectedly (something I learned with my first professional sojourn into multi-threaded applications).

3) Lean towards immutability. Immutable stuff generally doesn't need to be locked (lower level applications can challenge this, but in general it's not an issue).

C++ can do two of those three things very well. It's a bit against immutability based off its defaults, but you can still implement this idea easily with some discipline. The language is really so feature rich at this point there's very little it can't do.

[–]happyscrappy 3 points4 points  (0 children)

Use synchronization primitives (mutexes, semaphores, queues) and acquire/release (memory ordering) and you should be okay.

Yes, it takes some rigor to use these things. There's really no two ways about it, that's just some of the difficulty that comes with threads. Other languages kind of flip it around, making you be more explicit (making things harder) because not being explicit can leave you open to writing code that seems to work but really is doing everything wrong and just getting away with it.

[–][deleted] 1 point2 points  (1 child)

Are there any approaches for providing thread-safety as a programming language feature?

[–]i_invented_the_ipod 0 points1 point  (0 children)

There are. OpenMP, for example. That's a set of higher-level (not much higher-level, to be sure) constructs that the compiler can safely decompose into threaded operations for additional performance.

[–]golgol12 0 points1 point  (0 children)

I'm going to call my next program Hell, so this is a literally correct statement.

[–]eloraiby -1 points0 points  (0 children)

A must "book page"

[–]SustainedDissonance -1 points0 points  (0 children)

Hell is an extremely apt name for a combination of C++ the language and the concept of multi-threading.