Threads

You may also want to read the std::mutex article.

In the C++11 standard, the discussion of std::thread opens with "These threads are intended to map one-to-one with operating system threads." In other words, however the concept of a thread is mapped to the operating system's scheduler and kernel, within a C++ program we will call this thing a std::thread. I only wish that the same thing had been done with sockets. But, we can't have everything, and perhaps it was felt that BSD and POSIX cover sockets well enough.

There are multiple ways to fire up a thread. To some extent, the process resembles fork() to start up processes, but a thread is a jumping off point into a function from which we will someday return to the starting point, rather than continuing on our own. Threads share memory within their process; processes do not (unless you make it so).

Because the standard does not offer a completely worked example (and that's not its goal), the following code was taken from Digital Gaslight, Inc.'s LexĂ­meter product with the intent of providing a consistently usable example of a typical use. You are free to adapt it directly without worry that you are infringing on the ownership of the rest of the code and the product.

287   auto p = gAlgorithmTable.lower_bound("algorithm00");

298   std::vector<std::thread> vThreads;
299   auto pvThread = vThreads.begin();

325   while (p != gAlgorithmTable.upper_bound("algorithm99"))
326   {
327     vThreads.push_back(thread(p->second, m));
328     p++;
329   }

334   try
335   {
336     pvThread = vThreads.begin();
337     while (pvThread != vThreads.end())
338     {
339       pvThread->join();
340       pvThread++;
341     }
342   }

346   catch (std::system_error & e)
347   {
348     m["error"] <<= UNIQUE_ERROR_CODE;
349     switch (e.code().value())
350     {
351       case errc::resource_deadlock_would_occur:
352         m["errormessage"] <<= string("resource deadlock: ") + e.what();
353         break;
354       case errc::no_such_process:
355         m["errormessage"] <<= string("no such process: ") + e.what();
356         break;
357       case errc::invalid_argument:
358         m["errormessage"] <<= string("invalid argument: ") + e.what();
359         break;
360       default:
361         m["errormessage"] <<= e.what();
362         break;
363     }
364   }

So, how does this work?

Our program has a table of pointers to functions where simple strings are the keys (gAlgorithmTable). As you may gather, the algorithms are named cleverly as "algorithm00," "algorithm01," and so on. Because the algorithms process a const data structure, they are ideal candidates for threading as they do different things and do not crash into each other.

std::thread can be placed in the STL containers (cheer!), so this feature is exploited in lines 325 - 329 where the threads are kicked off by calling the c-tor, and pushing the constructed thread onto the back of the vector (line 327).

It should be clear, although not obvious, that the algorithms all take the same arguments because to be stored in a table the pointers to those algorithms would all need to be of the same type. The invocation syntax of thread looks like a function call in which the first argument is the function to be threaded, and the remaining arguments are the arguments of the threaded function. In other words, if we were calling algorithm03 directly, we would dereference the pointer to it, supply the arguments, and write:

(*(gAlgorithmTable["algorithm03"]))(m);

Starting up the threads is easy, and the constructor for std::thread does not throw exceptions. Now we need to wait for them to complete, and here there may be exceptions. In lines 336 - 341, the code marches through the vector of threads calling the join() function of each one. The surrounding try-block is present to catch the exceptions.

One simplification in the code example is that the threader (is this the right term?) needs to wait for all the algorithms to complete before it can summarize their results. Consequently, it makes no difference in what order the threads complete, and the code you see here will only reach the end of the vector when the longest running thread has finished.

Like other pseudo-system calls, when something goes wrong a std::system_error is thrown. Based on the value of the code in the object we take the appropriate branch in the switch statement (Line 349). There isn't much you can do when a thread dies, and in this case we are simply attaching a text shred to the "m" object for debugging or curiousity.

 

Last updated 2014-07-19T15:44:11+00:00.

Links to the standard

Threads are covered in section 30.3. When consulting the standard for this language feature more than any other, a reader should keep in mind that the standard is not a "how to" guide for concurrent multi-tasking. The std::thread interface is quite simple and direct, and it is covered in about five pages.

Benefits

Once again, we have a chance to write portable software across hardware and operating systems without having to learn the kernel interface for each, and fill our code with #ifdef.

Risks

Providing such a transparent interface to such a complex topic is a bit like giving everyone a gun without target practice. There are certain to be a number of feet shot off.