C, C++/C, C++ 잡지식

CreateThread와 _beginthreadex 간단하게 알아보기

Basaeng 2025. 10. 8. 21:11

https://basaeng.tistory.com/70

 

[WINDOWS VIA C/C++] 06.스레드의 기본

https://www.hanbit.co.kr/store/books/look.php?p_code=B2974835990 제프리 리처의 Windows via C/C++(복간판)이 책은 윈도우 XP, 윈도우 비스타, 윈도우 서버 2008까지 내용을 포괄한다. 이미 윈도우 10이 출시된 지 오래

basaeng.tistory.com

해당 포스트에서는 간단하게만 언급한 주제인데 복습겸 더욱 알아볼겸 포스팅해보겠습니다.

(내용은 제프리 리처의 WINDOWS VIA C/C++을 참고하였습니다.


CRT(C RunTime Library)

C/C++프로그램은 CRT에 의존합니다.

 

CRT는 언어차원에서 사용하는 구현체입니다.

예전에는 특정 함수들이 multithread-safe하지 않아 multithread환경에서는 사용할 수 없는 경우도 있었지만, 이제는 multithread-safe한 함수들이 표준(혹은 대체함수 사용)으로 사용되고 있습니다.

 

다만 이러한 함수들은 TLS 등 스레드마다 가질 수 있는 환경을 사용하기 때문에 thread가 생성되면 CRT의 관련된 데이터들에 대한 초기화가 필요합니다. 


CreateThread의 문제점

CreateThread는 Windows API함수입니다.

thread를 만들고 이후에 CRT 함수를 사용하기 위해서는 CRT초기화가 필요하지만 API차원에서는 CRT를 알 수 없기 때문에 CreateThread 만으로는 새롭게 만들어진 Thread 내에서 CRT를 사용할 수 없습니다.

 

물론 CRT를 아예 사용하지 않는다면 상관이 없을 수도 있지만 그런 경우는 애초에 거의 없고 함수를 통일하는 것이 개발에도 편할 것이기 때문에 CreateThread대신 _beginthreadex를 사용해야 합니다.

https://learn.microsoft.com/en-us/cpp/error-messages/tool-errors/c-runtime-error-r6016?view=msvc-170

 

C Runtime Error R6016

Learn more about: C Runtime Error R6016

learn.microsoft.com

https://learn.microsoft.com/en-us/windows/win32/procthread/creating-threads

 

Creating Threads - Win32 apps

Review how to use the CreateThread function to create a new thread for a process. Examine a code example that shows its usage.

learn.microsoft.com

여러 공식 문서에서도 CreateThread 대신 _beginthreadex를 사용할 것을 권장하고 있습니다.


_beginthreadex

_beginthread함수도 존재하지만 레거시 함수이고 다른 기능들과 호환을 위해 사용하지 않습니다.

 

_beginthreadex는 CRT를 초기화하며 스레드를 생성하는 함수입니다.

thread.cpp에 존재하는 _beginthreadex의 함수를 먼저 보고 차례차례 알아보겠습니다.

extern "C" uintptr_t __cdecl _beginthreadex(
    void*                    const security_descriptor,
    unsigned int             const stack_size,
    _beginthreadex_proc_type const procedure,
    void*                    const context,
    unsigned int             const creation_flags,
    unsigned int*            const thread_id_result
    )
{
    _VALIDATE_RETURN(procedure != nullptr, EINVAL, 0);

    unique_thread_parameter parameter(create_thread_parameter(procedure, context));
    if (!parameter)
    {
        return 0;
    }

    DWORD thread_id;
    HANDLE const thread_handle = CreateThread(
        reinterpret_cast<LPSECURITY_ATTRIBUTES>(security_descriptor),
        stack_size,
        thread_start<_beginthreadex_proc_type, true>,
        parameter.get(),
        creation_flags,
        &thread_id);

    if (!thread_handle)
    {
        __acrt_errno_map_os_error(GetLastError());
        return 0;
    }

    if (thread_id_result)
    {
        *thread_id_result = thread_id;
    }

    // If we successfully created the thread, the thread now owns its parameter:
    parameter.detach();

    return reinterpret_cast<uintptr_t>(thread_handle);
}

security_descriptor: 다른 함수들과 비슷하게 SECURITY_ATTRIBUTES를 사용해 보안 속성을 지정합니다.

stack_size: 스택의 크기를 바이트단위로 지정합니다. 0을 넣는다면 기본 스레드 스택 크기를 확보합니다.

procedure: 스레드 진입 함수에 대한 함수 포인터입니다.

context: procedure에 사용되는 파라미터 포인터입니다.

creation_flags: 스레드 생성동작을 제어하는 플래그입니다.

thread_id_result: 생성되는 스레드의 tid를 받기 위한 포인터입니다.

 

먼저 시작 함수와 파라미터의 유효성에 대해 체크합니다. 

이후 CreateThread를 호출합니다.


thread_start

CreateThread는 thread를 실행하면 시작할 함수와 파라미터를 넘깁니다.

_beginthreadex에서는 시작 함수로 thread_start를 지정하고 파라미터로는 실제 시작함수와 파라미터를 묶은 구조체를 넘겨 사용합니다.

template <typename ThreadProcedure, bool Ex>
static unsigned long WINAPI thread_start(void* const parameter) throw()
{
    if (!parameter)
    {
        ExitThread(GetLastError());
    }

    __acrt_thread_parameter* const context = static_cast<__acrt_thread_parameter*>(parameter);

    __acrt_getptd()->_beginthread_context = context;

    if (__acrt_get_begin_thread_init_policy() == begin_thread_init_policy_ro_initialize)
    {
        context->_initialized_apartment = __acrt_RoInitialize(RO_INIT_MULTITHREADED) == S_OK;
    }

    __try
    {
        ThreadProcedure const procedure = reinterpret_cast<ThreadProcedure>(context->_procedure);
        if constexpr (Ex)
        {
            _endthreadex(procedure(context->_context));
        }
        else
        {
            procedure(context->_context);
            _endthreadex(0);
        }
    }
    __except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
    {
        // Execution should never reach here:
        _exit(GetExceptionCode());
    }

    // This return statement will never be reached.  All execution paths result
    // in the thread or process exiting.
    return 0;
}

__acrt_thread_parameter*로 캐스팅한 뒤 _procedure(entry function)과 _context(parameter)를 사용하는 것을 알 수 있습니다.

 

__acrt_getptd()->_beginthread_context에서 crt초기화 작업을 실행합니다.

 

이후 procedure(context->_context)를 통해 실제 스레드 시작함수를 실행합니다.

스레드가 반환된 이후에는 _endthreadex를 통해 스레드를 반환하며, crt할당에 대한 해제도 내부적으로 해주고 있음을 알 수 있습니다.


std::thread의 시작 함수 _Start의 내부에서도 _beginthreadex를 사용합니다.

    template <class _Fn, class... _Args>
    void _Start(_Fn&& _Fx, _Args&&... _Ax) {
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));

        if (_Thr._Hnd) { // ownership transferred to the thread
            (void) _Decay_copied.release();
        } else { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
    }

결론

_beginthreadex를 사용하자 + std::thread()도 내부적으로 _beginthreadex를 사용한다.