EDIT: Having figured it out (see my own answer), I get that the question text below is misleading, which caused a lot of misunderstanding. I still believe it might help somebody who has the same issue.
Justification
I am asking this question (at least in its essence) now the third time, since the first and second attempt drifted off into discussions about details I did not intend.
I am nonetheless convinced that an answer to the actual core question would be a valuable contribution to people (like myself) who are not experts in multi-thread programming, since it is currently not easily found on the internet and poses a valid problem.
Question
Say we have a function fct
to be run in parallel using threads. Now, it is OS-dependent if a thread can be spawned at a given moment, depending on the available RAM and other things. If it’s not possible, and we still naively try to do so, the program will crash. So, how can we reliably decide if a thread can be spawned, and otherwise decide against it, such that the program doesn’t crash uncontrollably?
(As others pointed out, the possible number of concurrently running threads is “usually” “very high”. I have two different Windows machines, though, one with 48GB of RAM (Windows 10; VS 2019), the other with 64GB of RAM (Windows 11; VS 2022) for both of which this number is somewhere around <20 for the below (extremely simple!) example. For the actual, more complex code I try to improve, I am stuck with only 5 threads. The point is not to improve this number or find out why my machines are shitty. I rather want my code to run crash-free on any OS it encounters.)
To have a ground truth, consider this MWE:
#include <iostream>
#include <thread>
#include <vector>
#include <string>
constexpr int NUM_JOBS{ 100 }; // Number of jobs to run.
constexpr int NUM_THREADS{ 15 }; // Number of threads run at most in parallel. Setting this to 20 crashes on my machine.
void fct(const std::string& str) // Some function to run in parallel.
{
std::cout << "Processing job: " + str + "n";
}
int main(int argc, char* argv[])
{
std::vector<std::string> job_descriptions{}; // Different "configs" to call fct.
std::vector<std::thread> threads{};
int numThreads{ 0 };
for (int i = 0; i < NUM_JOBS; i++) { // Create some "configs".
job_descriptions.push_back("J" + std::to_string(i));
}
for (const auto& job : job_descriptions) { // Run the jobs, in parallel if possible.
bool spawned{ false };
while (!spawned) {
if (numThreads < NUM_THREADS) { // Spawn new threads only if not more than NUM_THREADS are running simultaneously.
threads.emplace_back(std::thread(fct, job));
spawned = true;
numThreads++;
}
else if (!threads.empty()) { // Otherwise wait for the topmost thread to finish before spawning new ones.
threads.front().join();
threads.erase(threads.begin());
numThreads--;
}
}
}
for (auto& t : threads) t.join(); // Join the remaining threads in the end.
return 0;
}
The function fct
to run in parallel is very simple, it only prints one line to cout. Using some constant NUM_THREADS
, we can control how many jobs are run in parallel at most. Setting this number too high yields on my Windows machines:
Exception thrown at 0x768BA892 in ***.exe: Microsoft C++ exception: std::system_error at memory location 0x06DCFA68.
(On Linux, I can set it very high, but that’s, as said, not the point.)
The question is basically, can we replace the condition numThreads < NUM_THREADS
with something to the effect of: “If thread can be spawned”?
25
it is OS-dependent if a thread can be spawned at a given moment, depending on the available RAM and other things. If it’s not possible, and we still naively try to do so, the program will crash.
This might technically be true, but in practice, it is not something that anyone worries about, especially on modern desktop computers running Windows (as opposed to tiny embedded systems.) Millions of programmers all over the planet are writing code every day that spawns threads with reckless abandon and without the slightest worry or concern. If you are the only one encountering issues, then clearly, there must be something wrong with what you are doing.
As others pointed out, the possible number of concurrently running threads is “usually” “very high”. I have two different Windows machines, though, […] for both of which this number is somewhere around <20 for the below (extremely simple!) example.
I do not even need to look at your example below, I can tell you with confidence that the number is nowhere around <20, it is thousands upon thousands, and the problem lies in all certainty in your example below. That is immeasurably more probable than both of your machines being “shitty”.
As for the fact that it runs on Linux but not on Windows, this can be fully explained by undefined behavior. If your code triggers undefined behavior, then it may work on one system and not on another, or it might even, by sheer coincidence, work on both systems; it is still wrong.
Now, there is a way to look at the question which is valid: Suppose you are working on a puny, claustrophobic embedded system that does really have a very low limit on the number of threads that it can spawn; how do you know if you can spawn a thread?
The answer here is that if the firmware of the system is written correctly, and if the C++ runtime for that system is written correctly, and if your code is written correctly, then you are not going to get some random crash; instead, it is the constructor of std::thread
that will throw an exception.
According to the documentation:
Exceptions
3) std::system_error if the thread could not be started. The exception may represent the error condition std::errc::resource_unavailable_try_again or another implementation-specific error condition.
So, be sure to catch that exception.
4
Suppose you have a function, bool threadCanBeCreated()
. And suppose you call it:
if (threadCanBeCreated()) {
createAThread();
}
else {
doSomethingElse();
}
Will it be possible for createAThread()
to create a thread? Would it be possible for doSomethingElse()
to create a thread?
If the value returned by threadCanBeCreated()
could be changed by the action of any other thread in the same program or, by any other process running on the same host, then the answer to both questions is, there is no way of knowing.
After threadCanBeCreated()
returns, but before the caller does something about the return value, the answer could change.
1
I FINALLY figured it out, and I’m making this an answer since it could help somebody coming across, as desperate as I have been.
3CxEZiVlQ’s comment was on the spot:
It looks very strange. Having 48GB and 64GB PCs you run 32-bit applications.
The problem seems to be that I compiled in 32-bit. Changing it to 64-bit I’m able to spawn “as many threads as I like”.
(Now, technically this is not an answer to my original question. But I guess for most practical cases this solves all the problems. I still don’t know how to catch the exception in the 32-bit case… But being able to spawn a high number of threads made this question far less important. And at least I understand now, why this was only happening to me and to nobody else on earth…)
3