Note: Since I decided to write a series about multithreading in C/C++, I’m going to rewrite this article.
Introduction
Talking about multithreading in C/C++, there are two standards that specify the APIs for multithreading:
- POSIX Threads: commonly known as Pthreads, is a part of the POSIX Standard.
- ISO C++ (since C++11): yes, C++ now has built-in support for multithreading, I'm going to call it C++ Thread for short.
In C, you can use Pthreads. In C++, you have a choice between Pthreads and C++ Thread. The implementations for the two standards can be found on most major systems, from PC (macOS, FreeBSD, Linux, ...) to automotive (AutoSAR Adaptive Platform, ...). If you're an Android platform developer and working on AOSP, you might have been working with them already. If your system doesn't support Pthreads, you have an option to use platform-specific thread APIs in C, or you can write in C++ and do multithreading using C++ Thread.
In this post, I'm going to show you some basic examples of both standards to create and launch a new thread. I also remind you to handle the error which might happen. I assumed that you already know how to compile these examples. If you don't, leave a comment.
Launching a thread using Pthreads
We call the function pthread_create
to create a new POSIX thread and launch it. The declaration of the function is:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
By presenting it this way, I mean that pthread_create
is declared in the header file pthread.h
and it needs 4 parameters to create a new thread:
-
pthread_t *thread
is the pointer to the thread identifier which we are about to receive, so it cannot beNULL
. -
const pthread_attr_t *attr
is the attribute object. If it'sNULL
, the default attributes are used. The newly created thread will have these attributes set bypthread_create
. -
void *(*start_routine)(void *)
is the function to be executed in the new thread, so it cannot beNULL
. -
void *arg
allows us to pass an argument to thestart_routine
function. Because its type is declared asvoid *
, you can pass any value which has the same size assize_t
, or a pointer to any type of object. Ifarg
is a pointer, you must make sure the data thatarg
points to remains valid during the execution of thestart_routine
function.
pthread_create
returns 0
if it successfully created a new thread. Otherwise, an error number will be returned to indicate the error. These numbers are defined in errno.h
. The error numbers that pthread_create
can return are:
-
EAGAIN
: The system lacked the necessary resources to create a new thread, or the process reaches its quota (PTHREAD_THREADS_MAX
). -
EPERM
: The caller does not have permission to set the required scheduling parameters or scheduling policy. -
EINVAL
: Invalid attribute(s).
It's bad practice if you don't check and handle the error number that pthread_create
returns. But for the sake of complexity, in the following example, I'd leave the error handling for the caller of the launchMyThread()
function:
#include <pthread.h>
#include <stdio.h>
pthread_t myThread;
/**
* `myThread`'s routine
*/
void *doSomething(void *arg) {
(void)(arg); /* To avoid a compiler warning that said argument `arg` is unused */
printf("[%s:%d %s]\n", __FILE__, __LINE__, __FUNCTION__);
/* In this example, `doSomething` just returns `0` to indicate a successful execution */
return (void *)0;
}
/**
* Launch myThread
* @return 0 : success
* non-zero : failure, see errno.h
*/
int launchMyThread() {
/* `myThread`'s initial attributes */
pthread_attr_t attr;
int exitCode = 0;
pthread_attr_init(&attr);
/* one of `myThread`'s attributes will be joinable, meaning the calling thread must wait for it to return */
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
exitCode = pthread_create(&myThread, &attr, &doSomething, NULL);
/* the caller must handle the error */
return exitCode;
}
There are two Pthreads functions that I have yet to mention: pthread_attr_init()
, pthread_attr_setdetachstate
. They will be covered in an upcoming post.
Now, move on to the next section to see how to achieve the same using C++ Thread.
Launching a thread using C++ Thread
C++ provides the std::thread
class (defined in <thread>
) to manage the thread of execution. To create a thread, we construct an object of the std::thread
class. The constructors look like this:
thread() noexcept; // Default constructor
template<typename Callable,typename Args...>
explicit thread(Callable&& func, Args&&... args); // Second constructor
thread(thread const& other) = delete; // No copy constructor
thread(thread&& other) noexcept; // Move constructor
The first constructor constructs an object, but it's not associated with any thread of execution. It is there for constructing an object without anything to execute.
The second one constructs an object that's associated with a thread of execution because it has enough information to do so:
-
Callable&& func
: an callable object. -
Args&&... args
: some arguments. They're optional. You may pass no argument.func
and each element ofargs
must beMoveConstructible
.
Note
AMoveConstructible
(since C++11) object is a object that can be constructed from a rvalue argument.
The&&
operator, in this case, is the rvalue reference declarator.
But what exactly are they? What do they do here? Well, I will cover this in another post called Move Semantics in C++.
Let's take a look at the following example:
#include <iostream>
#include <system_error>
#include <thread>
class BackgroundTask {
public:
void doSomething();
void doAnotherThing();
void operator()() const {
doSomething();
doAnotherThing();
}
};
int launchMyThread() {
try {
BackgroundTask task;
// `task` is copied into `myThread`'s storage, so make sure the copy behaves
// equivalently to the original
std::thread myThread(task);
} catch (std::system_error error) {
std::cout << error.what() << std::endl;
return -1;
}
return 0;
}
This is not the simplest example of launching a thread using C++ Thread. But it sure shows some differences between C-style code and C++ style. First, it constructs a BackgroundTask
object, which is callable because the class overrides the function call operator ()
. Of course, you can also pass a function, or a lambda expression, anything that is callable. The callable object is copied into the storage belonging to the newly created thread of execution and invoked from there. Therefore, you need to ensure that the copy behaves equivalently to the original, or the result may not be what you expect.
The example also shows that is optional to pass arguments. But if you need to, you can pass more than one argument, unlike pthread_create
, which only allows you to pass only one argument. To receive arguments, the class BackgroundTask
needs to override the function call operator with parameter(s):
#include <iostream>
#include <string>
#include <system_error>
#include <thread>
class BackgroundTask {
public:
void operator()(std::string_view name) const {
std::cout << "Hello " << name << std::endl;
}
};
int launchMyThread() {
try {
BackgroundTask task;
std::thread myThread(task, "Multithreading World");
} catch (std::system_error error) {
std::cout << error.what() << std::endl;
return -1;
}
return 0;
}
It's also bad practice if you don't do the error handling, in this case, exception handling. The second constructor throws a std::system_error
exception if it's unable to start the new thread, or if there's an exception while copying func
or args
into local storage.
📝 Never forget to handle errors or exceptions. Spend more time writing error/exception handling and you will spend less time investigating bugs.
So that's how you create and launch a new thread of execution, using Pthreads/C++ Thread. There's more about multithreading in C/C++. Stay tuned for updates!
Top comments (0)