DEV Community

Hoàng Văn Khoa
Hoàng Văn Khoa

Posted on • Edited on

Launching a thread in C/C++

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);
Enter fullscreen mode Exit fullscreen mode

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 be NULL.
  • const pthread_attr_t *attr is the attribute object. If it's NULL, the default attributes are used. The newly created thread will have these attributes set by pthread_create.
  • void *(*start_routine)(void *) is the function to be executed in the new thread, so it cannot be NULL.
  • void *arg allows us to pass an argument to the start_routine function. Because its type is declared as void *, you can pass any value which has the same size as size_t, or a pointer to any type of object. If arg is a pointer, you must make sure the data that arg points to remains valid during the execution of the start_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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 of args must be MoveConstructible.

Note

A MoveConstructible (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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)