विषय पर बढ़ें

अंतिम अद्यतन:2026-10-12

C++ std::thread और jthread

thread और jthread का संपूर्ण विश्लेषण

1. तैयारी

इस लेख की सामग्री gcc-13 के source code पर आधारित है, इसमें C++11 std::thread के उपयोग और kernel implementation की विस्तृत व्याख्या, pthread + thread के mixed उपयोग, और C++20 jthread के उपयोग तथा kernel implementation की व्याख्या की गई है।

अगर आप multithreaded development में नए हों या expert, यह लेख सीखने के लिए दोनों के लिए बहुत उपयुक्त है, इसमें basic theoretical knowledge भी शामिल है और समृद्ध practice तथा kernel code का खुलासा भी।

Source code विश्लेषण का version: gcc-13

https://github.com/gcc-mirror/gcc/tree/releases/gcc-13

रूपरेखा इस प्रकार है:

2. C++11 का thread

2.1 मूल उपयोग

एक thread बनाएँ, और उसमें अपना execute करने वाला function, parameters आदि पास करें।

यहाँ result के लिए atomic भी उपयोग कर सकते हैं और promise भी।

मूल उपयोग में आमतौर पर t.join का ध्यान रखना होता है, जिससे main thread child thread का इंतज़ार करे, यह current thread को block करता है, जब तक join() call करने वाला thread अपना निष्पादन पूरा न कर ले।

std::atomic<int> result(0);

void threadFunction(int a, int b) {
    result = a + b;
    std::cout << "Child thread finished its work.\n";
}

int main() {
    int a = 5;
    int b = 7;

    std::thread t(threadFunction, a, b);

    t.join();
    std::cout << "Result computed by the child thread: " << result << std::endl;

    return 0;
}

अगर आपने thread पर join() या dettach() निर्दिष्ट नहीं किया, तो runtime error आएगा, क्योंकि main/current thread अपना निष्पादन पूरा कर लेगा, जबकि बनाया गया दूसरा thread अभी भी चल रहा होगा।

उदाहरण: ऊपर से t.join() हटा दें, तो आपका program crash हो जाएगा।

Result computed by the child thread: 0
Child thread finished its work.
terminate called without an active exception
[1]    52814 abort      ./a.out

2.2 STL implementation

आप std_thread.h फ़ाइल देख सकते हैं, और मेरे साथ मिलकर kernel implementation पढ़ सकते हैं।

Code path: gcc/libstdc++-v3/include/bits/std_thread.h

2.2.1 pthread से संबंध

सबसे पहले यह एक class thread है, और एक काफी ज़रूरी type है, जैसे मेरे Linux system पर यह macro open है, डिफ़ॉल्ट रूप से आमतौर पर __gthread_t होता है, और यह आमतौर पर एक pthread_t है।

#ifdef _GLIBCXX_HAS_GTHREADS
    using native_handle_type = __gthread_t;
#else
    using native_handle_type = int;
#endif

यानी:

typedef pthread_t __gthread_t;

इसलिए हम पहला निष्कर्ष निकाल सकते हैं: underlying implementation Linux के pthread पर आधारित है।

2.2.2 आंतरिक id

thread class का एक private member ._M_id है।

private:
    id              _M_id;

इसका type आंतरिक class है, और उसमें और गहराई में native_handle_type है।

class id
{
  native_handle_type    _M_thread;

public:
  id() noexcept : _M_thread() { }

  explicit
  id(native_handle_type __id) : _M_thread(__id) { }

}

ऊपर की सामग्री के साथ मिलाकर, हम पाते हैं:

private:
    pthread_t               _M_id;

परिचित लगता है न? हमें दूसरा ज्ञान-बिंदु मिला: thread आंतरिक रूप से एक id maintain करता है, जो Linux पर डिफ़ॉल्ट रूप से pthread_t है।

2.2.3 Joinable है या नहीं

अगर कोई thread "joinable" अवस्था में है, तो उसके निष्पादन पूरा होने से पहले program उस thread के निष्पादन पूरा होने का इंतज़ार करने के लिए join() function call कर सकता है।

joinable का निर्णय आधार है:

_M_id != id()

पहले की सामग्री के आधार पर, हम यह निकाल सकते हैं:

pthread_t _M_id;
pthread_t null_thread = pthread_t();
_M_id != null_thread;

यानी: joinable का निर्णय आधार यह है कि current thread डिफ़ॉल्ट thread id (0) के बराबर नहीं है।

join method बहुत ज़रूरी है, हम आमतौर पर इसका उपयोग करते हैं, इसका implementation सिद्धांत सरल रूप से इस प्रकार है:

  • Joinable है या नहीं
  • हाँ, pthread_join call करें (यहाँ gthread वास्तव में pthread के interface तक जाता है)
  • नहीं, या ऊपर join में error हो, तो exception EINVAL या join error फेंकें।
  • Non-joinable सेट करें, यानी: आंतरिक thread id को 0 सेट करें
thread::join()
{
  int __e = EINVAL;

  if (_M_id != id()) // 是否可joinable
    __e = __gthread_join(_M_id._M_thread, 0);

  if (__e)
    __throw_system_error(__e);

  _M_id = id(); // _M_id = pthread_t(),设置为0
}

join के बाद, अगर हम दोबारा join करें, तो ऊपर वाला EINVAL exception फेंकेगा, यानी: invalid argument।

terminate called after throwing an instance of 'std::system_error'
  what():  Invalid argument

2.2.4 Detach

join के समान, पर उल्टा, program को बताता है कि आप उस thread के निष्पादन की अब परवाह नहीं करते, implementation तर्क में call किए गए interface को छोड़ बाकी सब एक जैसा है, दोहराने की ज़रूरत नहीं।

detach आंतरिक रूप से pthread_detach उपयोग करता है।

void
thread::detach()
{
  int __e = EINVAL;

  if (_M_id != id())
    __e = __gthread_detach(_M_id._M_thread);

  if (__e)
    __throw_system_error(__e);

  _M_id = id();
}

2.2.5 Construction और destruction

2.2.5.1 Construction

उपयोग:

thread t1;

डिफ़ॉल्ट construction का kernel code:

thread() noexcept = default;

Parameters वाले constructor का उपयोग:

std::thread t2(threadFunction, a, b);

Parameters वाले construction का kernel code:

template <typename _Callable, typename... _Args,
          typename = _Require<__not_same<_Callable>>>
explicit thread(_Callable &&__f, _Args &&...__args) {}

आंतरिक implementation है:

_M_start_thread(_State_ptr(new _State_impl<_Wrapper>(
                std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),
            _M_thread_deps_never_run);

देखा जा सकता है कि core function _M_start_thread है, इसका code सरल रूप में:

const int err = __gthread_create(&_M_id._M_thread,
         &execute_native_thread_routine,
         state.get());
if (err)
  __throw_system_error(err);

और __gthread_create को pthread_create समझा जा सकता है, तो अब बात बिल्कुल साफ़ है न?

ध्यान दें कि यहाँ एक detail है: pthread_create को पास किए गए parameters में func, execute_native_thread_routine है, ऊपर का conversion pthread_create रूप में:

pthread_create (&_M_id._M_thread, NULL, &execute_native_thread_routine, state.get());

और execute_native_thread_routine एक extern "C" से लिपटा function है, जो C++ के function नामों को C linkage देता है।

तो ऊपर के constructor का तर्क बहुत साफ़ है, वह pthread_create call करके आंतरिक thread id initialize करता है, तो यहाँ एक प्रश्न है, user के पास किए गए function और parameters का क्या होता है?

यहीं State आता है, ध्यान देने वाले दोस्तों ने देखा होगा कि ऊपर pthread_create का आखिरी parameter सामान्य function parameter है: state.get(), और function है execute_native_thread_routine।

static void*
execute_native_thread_routine(void* __p)
{
  thread::_State_ptr __t{ static_cast<thread::_State*>(__p) };
  __t->_M_run();
  return nullptr;
}

_State एक interface है, जिसका implementation _State_impl है, देखा जा सकता है कि parameters और function यहाँ forward किए जाते हैं, और फिर _M_run से user का पास किया गया function execute होता है, execute करते समय _Callable का operator() call होता है, पहले हम जानते हैं कि बनाते समय _Wrapper साथ था, इसलिए _Callable बराबर _Wrapper है, और __Wrapper एक _Call_wrapper alias है, जो user के function और parameters को अंदर पास करता है।

using _Wrapper = _Call_wrapper<_Callable, _Args...>;

struct _State
{
  virtual ~_State();
  virtual void _M_run() = 0;
};
using _State_ptr = unique_ptr<_State>;


template <typename _Callable>
struct _State_impl : public _State
{
  _Callable _M_func;

  template <typename... _Args>
  _State_impl(_Args &&...__args)
      : _M_func(std::forward<_Args>(__args)...)
  {
  }

  void
  _M_run() { _M_func(); }
};

तो यहाँ तक, हम यह देख सकते हैं

template <typename... _Tp>
using _Call_wrapper = _Invoker<tuple<typename decay<_Tp>::type...>>;

यानी ऊपर _Call_wrapper के सभी parameters से references और modifiers हटाकर tuple के रूप में पास किया जाता है, यानी:

_Invoker<tuple<_Callable, Arg1, Arg2, ...>

फिर अंत में यह std::__invoke को पास होता है।

तो, मान लीजिए मैंने यह code लिखा:

_Call_wrapper<decltype(&add), int, int> invoker(&add, 3, 4);

यह invoke के ज़रिए आपके लिए add function को call करेगा।

invoke(_Fn&& __f, _Args&&... __args)

बेशक ऊपर call होता है उसका आंतरिक __invoke, दोनों में कोई अंतर नहीं, एक बाहर का interface है, एक अंदर का।

ऊपर की व्याख्या के बाद, हम संक्षेप में देख लेते हैं:

  • Parameters वाला constructor tid बनाता है, pthread_create के ज़रिए
  • pthread_create को पास किया गया function एक wrapped function है, जो आंतरिक रूप से State से चलता है
  • User function और parameters को thread में देकर, अंत में इस function का call std::__invoke से होता है।
2.2.5.2 Copy और move

thead ने copy construction, copy assignment को प्रतिबंधित किया है, केवल move construction और move assignment समर्थित हैं।

thread(const thread &) = delete;
thread &operator=(const thread &) = delete;

Move construction

thread(thread &&__t) noexcept
{
  swap(__t);
}

void
swap(thread &__t) noexcept
{
    std::swap(_M_id, __t._M_id);
}

Move assignment

thread &operator=(thread &&__t) noexcept
{
  if (joinable())
    std::__terminate();
  swap(__t);
  return *this;
}
2.2.5.3 Destruction

Destruction का सिद्धांत यह है कि आपके बनाए thread के resources का consume/release होना चाहिए, और अगर कोई thread joinable है, तो वह pthread_create हो चुका है, पर अभी उपयोग नहीं हुआ, ऐसी स्थिति में सीधे terminate होगा।

~thread()
{
  if (joinable())
    std::__terminate();
}

2.2.6 अन्य members

उदाहरण के लिए: thread का आंतरिक thread id लेना, thread के id का आंतरिक _M_thread।

id
get_id() const noexcept
{ return _M_id; }

native_handle_type
native_handle()
{ return _M_id._M_thread; }

2.2.7 Interview topics

1.Construction और destruction

तो यहाँ एक प्रश्न है: नीचे दिए code में कोई समस्या है क्या?

// test1.cc
int main() {
  thread t1;
}

// test2.cc
int main() {
    std::thread t2(threadFunction, a, b);
}

इसका उत्तर है: test1 सामान्य रूप से चलेगा, test2 crash!

कारण यह है: thread का डिफ़ॉल्ट constructor कुछ नहीं करता, जबकि parameters वाला constructor अंदर __gthread_create (यानी: pthread_create) call करके thread बनाता है, यानी आंतरिक _M_id._M_thread initialize हो जाता है, destruction के समय joinable check होता है, उस समय _M_id और id() बराबर नहीं होते, यानी: joinable = true, सीधे std::__terminate() और crash।

2.Move assignment

मान लीजिए अभी नीचे वाले दो functions f1, f2 हैं, क्या ये दोनों functions सामान्य रूप से चलेंगे? क्यों?

void f1() {
  std::thread t1(tfunc);
  std::thread t2;
  t2 = std::move(t1);
  t2.join();
}
void f2() {
  std::thread t1(tfunc);
  std::thread t2(tfunc);
  t2 = std::move(t1);
  t2.join();
}

इसका उत्तर है: f1 सामान्य रूप से चलेगा, f2 crash!

कारण यह है: f1 में t2 ने डिफ़ॉल्ट constructor उपयोग किया (कुछ नहीं करता), इसलिए t2 अपने आप non-joinable है, सुरक्षित रूप से move assignment उपयोग किया जा सकता है। लेकिन f2 का t2 joinable है (इसलिए constructor में tid initialize हुआ), और move assignment के समय joinable check होता है, अगर joinable है तो सीधे terminate, यह समझने में आसान है, क्योंकि t2 का अपना thread बन चुका है, अभी consume और उपयोग नहीं हुआ, और swap होकर बाहर चला गया, तो इस समस्या को कैसे ठीक करें?

वास्तव में बहुत सरल है: आगे एक join लगा दें:

t2.join();  // 新加的
t2 = std::move(t1);
t2.join();

कुछ लोगों को लग सकता है कि t2 पर दो बार join करने से क्या कोई समस्या नहीं होगी, लेकिन इस समय दूसरी बार का t2 वास्तव में t1 ही है, इसलिए इसे ऐसे समझा जा सकता है:

t2.join();
t1.join();

3. C++20 का jthread

C++20 के बाद std::jthread आया, यह शुरू किए गए thread को automatically join कर सकता है, और interrupt signal भेज सकता है।

3.1 मूल उपयोग

अगर हम पहले वाला thread उपयोग करें, तो नीचे वाला code core dump करेगा, क्योंकि destructor में joinable check होता है, और तब terminate होता है।

std::thread t{ [] {std::cout << "hello std::thread" << std::endl; } };

std::cout << "t.joinable(): " << t.joinable() << std::endl;

अगर C++20 का jthread उपयोग किया, तो यह समस्या नहीं आएगी, उदाहरण के लिए:

std::jthread tj{ [] {std::cout << "hello std::jthread" << std::endl; } };

std::cout << "tj.joinable(): " << tj.joinable() << std::endl;

इस समय आप पाएँगे कि सामान्य रूप से चलता है!

3.2 STL implementation

jthread, thread के अंदर ही है, इसका implementation पहले वाले thread पर आधारित है, source location: gcc/libstdc++-v3/include/std/thread

आखिर क्या राज़ है? आइए साथ मिलकर खोलते हैं।

3.2.1 thread से संबंध

jthread और thread गहराई से जुड़े हैं, कहा जा सकता है कि thread पर आधारित implementation है, जिसका सबसे केंद्रीय member thread type है, और आंतरिक id तथा native_handle_type types thread के आंतरिक types उपयोग करते हैं।

using id = thread::id;
using native_handle_type = thread::native_handle_type;

thread _M_thread;

3.2.2 stop_token और stop_source

jthread ने आंतरिक रूप से std::stop_token से control बढ़ाया है। std::stop_token thread के निष्पादन को stop request भेजने का एक mechanism है। यह request के रूप में चलता है, और अगर thread के निष्पादन में इसे सही ढंग से संभाला जाए, तो उसे terminate होने की अनुमति मिलती है।

आंतरिक रूप से std::stop_source member से maintain होता है।

stop_source _M_stop_source;

नीचे हम साथ मिलकर STL का implementation देखते हैं।

1.stop_source

सबसे पहले stop_source आंतरिक रूप से एक state maintain करता है, जिसे कहते हैं:

stop_token::_Stop_state_ref _M_state;

इसके अंदर यह है:

_Stop_state_t* _M_ptr

क्योंकि एक stop_source को अलग-अलग threads hold कर सकते हैं, इसलिए यह shared हो सकता है, इस कारण आंतरिक रूप से एक reference से यह किया जाता है, इसलिए आप source में _Stop_state_ref देख सकते हैं, जो पूरे state के lifecycle और ownership की ज़िम्मेदारी संभालता है। इसका shared implementation underlying _Stop_state_t से होता है, जिसके अंदर दो members बहुत ज़रूरी हैं:

using value_type = uint32_t;
std::atomic<value_type> _M_owners{1};
std::atomic<value_type> _M_value{_S_ssrc_counter_inc};

एक है _M_owners, जो ownership manage करता है; _M_value यह manage करता है कि कब रुकना है।

हम रोज़ जो तीन interfaces उपयोग करते हैं वे हैं:

  • stop_possible
  • stop_requested
  • request_stop

Underlying level पर _M_value में संगत bit सेट की जाती है

static constexpr value_type _S_stop_requested_bit = 1;
static constexpr value_type _S_locked_bit = 2;
static constexpr value_type _S_ssrc_counter_inc = 4;

जब user stop का अनुरोध करता है: request_stop, तब _S_stop_requested_bit सेट हो जाती है, और अगर user ने stop_requested call किया, तो भी यह आंतरिक _M_value में देखता है कि _S_stop_requested_bit सेट है या नहीं।

_S_locked_bit stop_possible interface से जुड़ा है, अगर stop request पहले ही भेजी जा चुकी है या अब भी stop request भेजी जा सकती है, तो stop_possible true return करेगा।

दूसरा है _S_ssrc_counter_inc, यह डिफ़ॉल्ट value है, stop_source के construction/copy construction के समय value में _S_ssrc_counter_inc जुड़ जाता है।

2.stop_token

ऊपर बताए _Stop_state_ref की सारी सामग्री लगभग stop_token के अंदर है, इसके अंदर भी एक state maintain होता है, जिसे कहते हैं:

_Stop_state_ref _M_state;

Implementation का detail ऊपर जैसा ही है, दोहराते नहीं। तो इसका stop_source से क्या अंतर है?

यहाँ cppreference की व्याख्या देते हैं: stop_token class यह check करने के तरीके देती है कि क्या उससे जुड़े std::stop_source object के लिए stop request भेजी जा चुकी है या भेजी जा सकती है। यह अनिवार्यतः संबंधित stop state का thread-safe "view" है।

https://en.cppreference.com/w/cpp/thread/stop_token

3.stop_callback

stop_callback और stop_token का संबंध यह है कि stop_callback, stop_token में register होता है, stop_token आंतरिक रूप से callback के deletion और addition के लिए एक doubly linked list structure maintain करता है, callback का insertion head insertion से होता है।

Callback trigger होने के दो समय हैं:

  • request_stop के समय trigger

Callback पहले से stop_token में register है, तो कुछ समय निष्पादन के बाद user request करता है, तब callback trigger होता है।

auto onStop = []() {
    std::cout << "Timer has stopped." << std::endl;
};

// Register the onStop callback with stop_callback
std::stop_callback callback(source.get_token(), onStop);
// do something for example: sleep

// stop
source.request_stop(); // invoke callback
  • Registration के समय तुरंत trigger

stop_token ने पहले ही stop मांग लिया है, ऐसी स्थिति में callback registration पर तुरंत trigger होता है, क्योंकि thread पहले ही निष्पादन पूरा कर चुका है, तब callback चलना चाहिए।

thread.request_stop();
std::stop_callback callback(stopToken, []() {
  std::cout << "Callback executed" << std::endl;
}); // invoke callback

STL में concrete implementation यह है कि _M_register_callback, __cb->_M_run() को call करता है, तब callback function call होता है।

3.2.3 Construction और destruction

3.2.3.1 Construction
  • डिफ़ॉल्ट construction

डिफ़ॉल्ट construction एक खाली stopstate initialize करता है, आंतरिक tid maintain नहीं करता, व्यवहार thread जैसा ही है।

jthread() noexcept
  : _M_stop_source{nostopstate}
  { }
  • Parameters वाला construction

_M_thread initialize करता है, यह _S_create के ज़रिए फिर _S_create_pmf call करके एक thread बनाता है, और उसे _M_thread को देता है, इस तरह आंतरिक thread construct हो जाता है।

template<typename _Callable, typename... _Args,
   typename = enable_if_t<!is_same_v<remove_cvref_t<_Callable>,
             jthread>>>
  explicit
  jthread(_Callable&& __f, _Args&&... __args)
  : _M_thread{_S_create(_M_stop_source, std::forward<_Callable>(__f),
      std::forward<_Args>(__args)...)}
  { }
3.2.3.2 Copy और move

Copy construction और copy assignment प्रतिबंधित कर दिए गए हैं, व्यवहार thread जैसा ही!

jthread(const jthread&) = delete;
jthread& operator=(const jthread&) = delete;

Move construction डिफ़ॉल्ट है, move assignment swap उपयोग करती है।

jthread(jthread&&) noexcept = default;

jthread&
operator=(jthread&& __other) noexcept
{
  std::jthread(std::move(__other)).swap(*this);
  return *this;
}

void
swap(jthread& __other) noexcept
{
  std::swap(_M_stop_source, __other._M_stop_source);
  std::swap(_M_thread, __other._M_thread);
}
3.2.3.3 Destruction

Destruction तय करता है कि jthread शुरुआत में कैसा व्यवहार करेगा, हमें manually join क्यों नहीं करना पड़ता, क्योंकि destruction के समय यह check करता है कि join किया जा सकता है या नहीं, अगर joinable है, तो user ने join नहीं किया, destructor आपके लिए join call कर देता है, साथ ही उससे पहले request_stop भी check करता है।

~jthread()
{
  if (joinable())
    {
      request_stop();
      join();
    }
}
3.2.3.4 अन्य members

अन्य members मूल रूप से thread पर operations हैं, और बाहर एक interface expose करते हैं, उदाहरण के लिए:

[[nodiscard]] bool
joinable() const noexcept
{
  return _M_thread.joinable();
}

join()
{
  _M_thread.join();
}

void
detach()
{
  _M_thread.detach();
}

[[nodiscard]] id
get_id() const noexcept
{
  return _M_thread.get_id();
}

[[nodiscard]] native_handle_type
native_handle()
{
  return _M_thread.native_handle();
}

[[nodiscard]] static unsigned
hardware_concurrency() noexcept
{
  return thread::hardware_concurrency();
}

बाकी कुछ stop_source के operations हैं:

[[nodiscard]] stop_source
get_stop_source() noexcept
{
  return _M_stop_source;
}

[[nodiscard]] stop_token
get_stop_token() const noexcept
{
  return _M_stop_source.get_token();
}

bool request_stop() noexcept
{
  return _M_stop_source.request_stop();
}

3.3 उदाहरण

cppreference का यह उदाहरण बहुत अच्छा है, उदाहरण में user के work function ने stop_source को parameter के रूप में पास किया, अंदर stop_requested() check करके, अगर रुक गया है, तो हम resources release जैसे operations कर सकते हैं, बहुत उपयोगी है।

Trigger की शर्त यह है कि main thread ने request_stop चला दिया।

https://en.cppreference.com/w/cpp/thread/stop_source

संबंधित विषय

टिप्पणियाँ