Comparison of C++ and Posix Threads

As most of us know the thread library was introduced in the C++11 standard along with a number of other language features and standard library components that were designed to support multithreaded programming, including atomic operations, mutexes, and condition variables. Together, these additions to the C++ standard library made it easier for developers to write correct and efficient multithreaded code in C++. The introduction of the thread's library was a significant addition to the C++ standard library and was motivated by the increasing importance of multithreaded programming in modern software development. Although these features were released in 2011, still I feel there is less familiarity w.r.t these new features. I am trying to uncover some basic concepts like why we need new feature as language lib when we already have POSIX, limitations, advantage, limitations ,etc. in this article.

It is useful to compare C++ threads and POSIX threads to understand the differences in functionality and features they provide. This knowledge can help in choosing the appropriate threading mechanism based on the requirements of the application being developed.

As said earlier C++ threads are a part of the C++ standard library and provide an object-oriented interface for creating and managing threads. They offer several useful features, such as support for thread-local storage, atomic operations, and the ability to wait on multiple threads. However, they may not be available on all platforms, and their implementation may vary between compilers.

On the other hand, POSIX threads are a standardized thread API for Unix-like operating systems. They provide a rich set of functionality, such as support for thread-local storage, synchronization primitives, and signal handling. They are widely available on most Unix-like systems, including Linux, macOS, and FreeBSD. However, they are not part of the C++ standard library and require more low-level programming compared to C++ threads.

By comparing C++ threads and POSIX threads, developers can understand the trade-offs and choose the appropriate threading mechanism based on the platform and requirements of their application.

Motivation for adding threads and other components as lang feature

Before the introduction of the threads library in C++11, C++ programmers had to rely on third-party libraries or platform-specific APIs to implement multithreading. The addition of the threads library to the C++ standard library provided a portable and standard way to implement multithreaded programming in C++, which made it easier for developers to write multithreaded code that could run on different platforms.

In addition to the threads library, C++11 also introduced a number of other language features and standard library components that were designed to support multithreaded programming, including atomic operations, mutexes, condition variables, and futures. Together, these additions to the C++ standard library made it easier for developers to write correct and efficient multithreaded code in C++.

How to create the thread using POSIX?

#include<pthread.h>
void *myThreadFunction(void *arg);

int main() {
    pthread_t myThread;
    pthread_create(&myThread, NULL, myThreadFunction, NULL);
    // ...
    pthread_exit(NULL);
}

void *myThreadFunction(void *arg) {
    // thread code here
    return NULL;
}        

How to create a thread in C++?

There are several ways to create threads in C++ using the standard library.

Let me not go into detail about various ways to create threads using C++ std lib , I will try to write a separate article on this topic. But let me list down few of them here.

  1. Using the function std::thread constructor:

Example below:

#include<iostream>
#include <thread>

void my_thread_function(int* pData) {
    std::cout << "Hello from my_thread_function! 
        and data passd ="<<*pData << std::endl;
}

int main() {
    int data = 101;
    std::thread my_thread(my_thread_function,&data);
    my_thread.join();
    return 0;
}        

2. Using Functor:

3. Using a member function and an object:

4. Using a lambda function:

5. Using a packaged task

6. Using an async function:

7. Using the function std::jthread constructor: Introduced from C++20.


How are C++ threads implemented internally?

It's very important to know how C++ std implements the threads internally to compare with POSIX threads and also to decide which thread to use .

C++ uses the native OS threads utilities for implementing the threads, mutexes, and other synchronization primitives provided by the standard thread library.

When you create a thread using the std::thread class in C++, the implementation will use the underlying threading API provided by the operating system on which your program is running. For example, on Windows, the implementation will use the Windows threading API(CreateThread()), while on Linux, it will use the POSIX threading API (pthread_create()).

Similarly, mutexes, condition variables, and other synchronization primitives provided by the standard thread library are also implemented using platform-specific APIs, such as Windows mutexes and POSIX semaphores.

Using the native OS threads utilities provides several benefits, such as better performance, improved scalability, and more efficient use of system resources. Additionally, it also ensures that the behavior of the C++ standard library is consistent with the behavior of the underlying operating system, which makes it easier to write portable and reliable code.

When C++ std threads are created, they can be implemented using a combination of user-level and kernel-level threads. The exact implementation details may vary depending on the specific operating system and C++ standard library implementation.

At a high level, the C++ standard library implementation may use user-level threads for some operations, such as thread creation, scheduling, and synchronization. User-level threads are typically faster and more lightweight than kernel-level threads, as they can be managed entirely in user space without requiring any kernel involvement.

However, in some cases, the C++ standard library implementation may need to use kernel-level threads for certain operations, such as blocking I/O operations or system calls. Kernel-level threads are managed by the operating system's kernel and can take full advantage of multiple processor cores, making them more efficient for certain types of operations.

In order to achieve a balance between performance and efficiency, the C++ standard library implementation may use a combination of user-level and kernel-level threads, depending on the specific operation being performed.

For example, the implementation may use user-level threads for lightweight operations such as mutex locking and unlocking, while using kernel-level threads for heavier operations such as I/O operations or waiting on condition variables.

The combination of user-level and kernel-level threads allows the C++ standard library implementation to provide a fast and efficient threading model that can take advantage of multiple processor cores and provide high scalability. However, the exact implementation details may vary depending on the specific platform and library implementation of the C++ standard library that is used by the program. Different C++ standard libraries may use different threading models. For example, libstdc++ on Linux typically uses pthreads for implementing threads, which are kernel-level threads, while on some platforms, such as macOS and iOS, libc++ uses user-level threads provided by the operating system.

How to check which lib C++ threads use?

Run the ldd command that shows the shared libraries that are linked to the executable file and their memory addresses.

ex a.out.

Output put in my case :

  • linux-vdso.so.1: a virtual dynamic shared object provided by the kernel, used by programs to access system calls faster than through normal libraries
  • libstdc++.so.6: the GNU Standard C++ Library version 6, which provides the standard library for C++ programs compiled with GCC
  • libgcc_s.so.1: the GCC support library, which provides low-level support for GCC-compiled programs, including exception handling and stack unwinding
  • libc.so.6: the C library, which provides the standard library for C programs. The POSIX threads library is implemented in libc.so.6, which is the C standard library for Linux systems, and it provides the necessary functions to create, synchronize and manage threads. So, libc.so.6 is the shared library that uses POSIX threads for thread creation in your case.
  • libm.so.6: the math library, which provides math functions such as sin, cos, and sqrt

It is better to refer to documentation for the C++ standard library and the documentation for the underlying operating system to understand the implementation details of the std::thread class and the threading API, respectively. This would help us to understand how the standard library is implemented and how it interacts with the underlying operating system


What is the difference between using the C++ std threads and POSIX threads?

This is the most important topic for application programmers.

  1. API: The API for C++ std threads and POSIX threads are different, with different function names and parameters. The C++ std thread library is part of the C++ standard library and provides a C++ interface, while the POSIX threads library is a separate library that provides a C interface.
  2. Implementation: As discussed earlier the implementation of C++ std threads and POSIX threads may differ depending on the specific platform and implementation details. C++ std threads are typically implemented using a combination of user-level and kernel-level threads, while POSIX threads are typically implemented using kernel-level threads.
  3. Portability: C++ std threads are part of the C++ standard library, which makes them more portable than POSIX threads, which are a separate library and may not be available on all platforms.
  4. Exception handling: C++ std threads support exception handling, while POSIX threads do not. This means that in C++ std threads, exceptions can be propagated across thread boundaries, while in POSIX threads, exceptions must be caught and handled within the same thread.
  5. Synchronization primitives: C++ std threads provide a set of synchronization primitives, such as mutexes, condition variables, and atomic operations, that are designed to work with the C++ language and its memory model. POSIX threads provide similar synchronization primitives, but they are designed to work with the C language and may require more low-level manipulation of shared memory.

How to decide which thread to use "C++ std thread or POSIX thread" ?

The decision of whether to use C++ std threads or POSIX threads (pthreads) depends on several factors, including the level of abstraction needed, the portability requirements of the code, and the familiarity of the programmer with the different thread models.

Here are a few factors to consider when deciding which thread model to use:

  1. Level of abstraction: C++ std threads provide a higher level of abstraction than POSIX threads, with support for features such as thread-local storage, lock-free algorithms, and thread-safe memory allocation. If you need these features or want to write more abstract, object-oriented code, C++ std threads may be a better choice.
  2. Portability: C++ std threads are part of the C++ standard library and are designed to be portable across different platforms and operating systems. If you need your code to be portable and want to avoid platform-specific APIs, C++ std threads may be a better choice. POSIX threads, on the other hand, are specific to POSIX-compliant operating systems and may not be available on all platforms.
  3. Performance: In some cases, POSIX threads may offer better performance than C++11 threads, especially when dealing with low-level system tasks or real-time applications.
  4. Familiarity: If you or your team are already familiar with POSIX threads and have experience working with them, it may be easier to use pthreads for your project. Similarly, if your code already uses a lot of POSIX-specific APIs, it may make sense to stick with pthreads for consistency.
  5. Ease of use: C++11 threads are generally considered to be easier to use and require less boilerplate code than POSIX threads. They also offer a more modern and object-oriented programming interface.
  6. Integration with other C++ features: C++11 threads integrate seamlessly with other C++ features like lambda expressions and std::future objects, making it easier to write complex multithreaded programs.
  7. Third-party libraries: If the application requires the use of third-party libraries, it may be more practical to use the type of threads that are used by those libraries. Some libraries may only support one type of threading, which could limit the options for the developer.
  8. Compatibility with existing code: If the application already has existing code that uses one type of threading, it may be easier and more efficient to continue using that type rather than converting the code to use a different threading model.
  9. Availability of resources and support: The availability of documentation, tutorials, and community support may also influence the choice of threading model. If one type of threading has more resources and support available, it may be easier for the developer to get help and troubleshoot issues.
  10. Security considerations: In some cases, the choice of threading model may affect the security of the application. Developers may need to consider the potential security implications of using one type of threading over another, such as the risk of race conditions or deadlocks.

  • Privilege level: POSIX threads can be used at the kernel level, which gives them greater access to system resources. This can be a security concern if the thread has more privileges than necessary. In contrast, C++ threads are typically implemented at the user level, which means they have limited access to system resources.
  • Resource isolation: If multiple threads share the same address space, there is a risk of one thread accessing or modifying data that belongs to another thread. This can be a security concern if the threads are running with different privileges or if sensitive data is being shared. POSIX threads allow for greater control over resource isolation than C++ threads, but this also makes them more complex to use.

11. Project requirements: Finally, the specific requirements of the project may also influence the choice of threading model. For example, if the application needs to be highly scalable and performant, the developer may choose a threading model that is optimized for these requirements.

What are Limitation of C++ threads?

  1. Lack of real-time support: C++ threads do not provide real-time guarantees, which means that it can be difficult to predict exactly when a thread will execute. If real-time performance is critical, developers may need to use a different threading model or implement additional synchronization mechanisms.
  2. Limited control over scheduling: C++ threads rely on the operating system's scheduler to allocate processor time to threads. While this generally works well, it means that developers have limited control over thread scheduling and may not be able to fine-tune performance in certain cases.
  3. Complexity: Multithreaded programming can be inherently complex, and C++ threads are no exception. Developers need to understand the underlying mechanisms of threads and synchronization to avoid common issues like race conditions and deadlocks.
  4. Debugging: Debugging multithreaded code can be challenging, and C++ threads are no exception. Developers may need to use specialized tools and techniques to identify and diagnose threading issues.
  5. Platform-specific behavior: While C++ threads are part of the C++ standard library, there can still be platform-specific differences in behavior. Developers need to be aware of these differences and may need to write platform-specific code in some cases.

What are the limitations of POSIX threads:

  1. Error handling: Error handling is not as robust in POSIX threads as it is in C++ threads. In POSIX threads, errors are reported through error codes, which can make it difficult to handle and debug errors.
  2. No support for RAII: POSIX threads do not support Resource Acquisition Is Initialization (RAII), which is a programming technique used in C++ to manage resources like memory and file handles. RAII can help simplify code and make it more robust.
  3. Limited support for thread-local storage: POSIX threads have limited support for thread-local storage(pthread_key_create(), pthread_setspecific(), and pthread_getspecific()), which is a way to store data that is unique to each thread. C++ threads have better support for thread-local storage through the thread_local keyword.POSIX threads have some limitations compared to C++ threads when it comes to TSD/TLS, such as:

  • TSD/TLS is implemented using a global key-value map, which can become a performance bottleneck in high-concurrency scenarios.
  • TSD/TLS cannot be automatically cleaned up when a thread exits, requiring the programmer to manually clean up any resources associated with the TSD/TLS.
  • TSD/TLS cannot be inherited by child threads, which means that any TSD/TLS set by a parent thread will not be available in its child threads.
  • In contrast, C++ threads provide a more robust and flexible implementation of TLS with the thread_local keyword, which allows for automatic cleanup and inheritance of TLS in child threads.

4. No support for move semantics: POSIX threads do not support move semantics, which is a C++ feature that allows objects to be moved instead of copied. Move semantics can be more efficient than copying and can help avoid unnecessary memory allocations.

5. No support for async/await: POSIX threads do not have built-in support for async/await, which is a C++ feature that allows for more efficient asynchronous programming. Async/await can make it easier to write code that is both asynchronous and easy to read and understand.


Is it OK to use both C++std and POSIX threads in the same project?

From the possibility point of view Yes, it is possible to use both C++ threads and POSIX threads in the same project. However, there are some implications, problems, and concerns that developers should be aware of:

  1. Complexity: Using two different threading models can increase the complexity of the code. Developers need to be familiar with both threading models and understand how they interact with each other to avoid issues like race conditions and deadlocks.
  2. Performance: Using multiple threading models can have an impact on performance. If the two threading models are not well-integrated, there could be additional overhead associated with switching between them.
  3. Compatibility: Some libraries or components may only support one threading model, which could limit the options for the developer. It's important to check the compatibility of all components before mixing threading models.
  4. Debugging: Debugging issues that involve multiple threading models can be challenging. Developers may need to use specialized tools and techniques to identify and diagnose threading issues.
  5. Maintenance: Using two different threading models could make the code harder to maintain. If developers leave the project, new developers may find it difficult to understand the code and may need to spend additional time learning how to work with multiple threading models.

Let me illustrate more on this with an example.

When developing a software system, multiple libraries or components may be used in the project. These libraries can be developed by different teams, using different programming languages and platforms, and with different threading models. It's important to check the compatibility of all these components before integrating them into the final product.

For example, let's say you are developing a system in C++ that uses the Boost library for some functionality. Boost uses the C++11 threading model, which includes the std::thread class for creating and managing threads. However, you also want to use a third-party library that uses the POSIX threading model, which includes the pthread_create function for creating threads.

If you mix these threading models in your application, you may run into compatibility issues. For example, if you create a std::thread in your C++ code and pass it to the POSIX library, the POSIX library may not be able to correctly interact with the thread. Similarly, if you create a POSIX thread in the third-party library and try to pass it to the Boost library, the Boost library may not be able to interact with it correctly also very difficult to debug, performance tuning and maintain ,etc.

To avoid these issues, it's important to ensure that all components in the system use the same threading model. If this is not possible, it may be necessary to write wrapper functions or classes to bridge the gap between the different threading models. This can be time-consuming and error-prone, so it's important to carefully consider the threading models used by all components before integrating them into the final system.

Overall, using two different threading models in the same project is possible, but it should be done carefully and with consideration for the potential implications and problems.

Conclusion: Tt is difficult to conclude which threading model is better, as both C++ threads and POSIX threads have their advantages and disadvantages, and the choice of which to use often depends on the specific requirements of the application. C++ threads provide a higher-level abstraction for multithreading, with more convenient syntax and better integration with the rest of the C++ standard library, while POSIX threads offer a more fine-grained level of control and more portability across different platforms. Ultimately, the choice between the two should be based on factors such as performance requirements, platform support, ease of use, and compatibility with other libraries and frameworks used in the application.

Thanks for reading till end .I hope you will find this article useful in some way !

If you have any questions or suggestions, please leave a comment in the comment section.

Jothi Gopal

C++ Cuda and AI Developer

1 年

good article to learn more information about thread

Adel Barkam

Embedded Vision developer & Teacher

1 年

Thanks for writing this useful guide

Conrad Lautenschlager

Computer science graduate & software developer | Java, C/C++, TypeScript | MySQL, MongoDB

1 年

Very clear and concise, thanks for writing.

Aliaksei Sanko

Senior Software Developer

1 年

To me, using POSIX threads in C++ looks like following some policy of deliberately creating non-portable code.

Rahul V.

Equity Derivatives IT Quant, Fixed Income Benchmarking-Lead Analyst

1 年

Good Article . Please correct Comparision spelling in header "Comparison"

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Atomics in Rust

    Atomics in Rust

    Atomics in Rust are fundamental building blocks for achieving safe concurrent programming. They enable multiple threads…

  • Frequently used Thread API - Random notes

    Frequently used Thread API - Random notes

    Thread Creation and Management: thread::spawn: Creates a new thread and executes a closure within it. It returns a…

  • Difference b/w Cell and RefCell

    Difference b/w Cell and RefCell

    Both Cell and RefCell are used in Rust to introduce interior mutability within immutable data structures, which means…

  • Tokio::spawn() in depth

    Tokio::spawn() in depth

    Tokio::spawn() is a function provided by the Tokio runtime that allows you to create a new concurrent task. Unlike…

  • tokio::spawn() Vs Async block Vs Async func

    tokio::spawn() Vs Async block Vs Async func

    Asynchronous programming is a powerful paradigm for handling I/O-bound operations efficiently. Rust provides several…

  • Tokio Async APIS - Random notes

    Tokio Async APIS - Random notes

    In this article, we will explore how to effectively use Tokio and the Futures crate for asynchronous programming in…

  • Reactor and Executors in Async programming

    Reactor and Executors in Async programming

    In asynchronous (async) programming, Reactor and Executor are two crucial components responsible for managing the…

  • Safe Integer Arithmetic in Rust

    Safe Integer Arithmetic in Rust

    Rust, as a systems programming language, emphasizes safety and performance. One critical aspect of system programming…

  • iter() vs into_iter()

    iter() vs into_iter()

    In Rust, iter() and into_iter() are methods used to create iterators over collections, but they have distinct…

  • Zero-cost abstraction in Rust

    Zero-cost abstraction in Rust

    Rust supports zero-cost abstractions by ensuring that high-level abstractions provided by the language and standard…

社区洞察