线程的创建和基本使用

原创
2022-12-02
4846
1

1. 什么是线程? 为什么要用多线程?

线程,是操作系统能够运行和计算和调度的最小单位。一个进程至少要包含一个线程,线程要包含在进程中, 是进程实际运作单位。一个进程中可以多有个线程,每个线程并发执行。每个线程有独立的栈空间,线程之间共享静态存储区和堆空间。

那么什么时候要用多线程呢? 当我们要使用并发的时候,就可以使用多线程。 比如,我们要想要解码音视频的同时去播放音频和视频;打印机打印PDF并且界面显示当前进度和提示信息等等。


2. 线程的创建和使用

2.1 使用操作系统API创建

无论是Windows还是Linux,都提供了创建线程的API提供给我们去创建线程。Windows中使用API CreateThread 创建一个线程。 它的具体表现形式如下:

HANDLE
WINAPI
CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
    );
  • lpThreadAttributes , 是线程的安全属性,一般设为 nullptr
  • dwStackSize , 表示线程栈空间的大小,单位为字节。一般设置为0,表示默认大小。
  • lpStartAddress , 表示新线程所执行的函数指针,类型为 LPTHREAD_START_ROUTINE 。其定义如下:
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
    LPVOID lpThreadParameter
    );
typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

即,一个参数 LPVOID 类型,返回值为 DWORD 类型的函数指针。 这里需要指出的是,函数需要使用 WINAPI 修饰, 也就是要使用 __stdcall 来修饰函数。

  • lpParameter ,传给线程的参数,LPVOID 类型本质上是 void * 。
  • dwCreationFlags ,一般设置为0,表示线程创建后立即执行。如果设置为 CREATE_SUSPENDED ,表示使用 ResumeThread 时,线程才开始执行。
  • lpThreadId ,输出参数,返回线程ID。
  • 返回值: 返回线程句柄,如果失败返回空(nullptr)。

下面是一个简单的示例:

#include <stdlib.h>
#include <iostream>
#include <Windows.h>

// 线程中执行的函数
DWORD WINAPI threadProc(LPVOID lpParameters)
{
    bool isRunThread = true;
    while (isRunThread)
    {
        static int number = 0;
        if (number >= 5)
        {
            // 仅执行5次打印
            isRunThread = false;
            break;
        }
        std::cout << "This is Run in Thread ID:" << ::GetCurrentThreadId() <<  ", Number is :" << number++ << std::endl;

        Sleep(200);
    }
    
    return 0;
}

// Windows API 创建线程
HANDLE createWindowsThread(DWORD *threadId)
{
    HANDLE handle = ::CreateThread(nullptr, 0, threadProc, nullptr, 0, threadId);

    return handle;
}

int main(int argc, char** argv)
{
    DWORD threadId = 0;
    HANDLE threadHandle = createWindowsThread(&threadId);
    if (threadHandle)
    {
        // 线程创建成功,打印创建的线程ID和主线程ID
        std::cout << "Create Thread Success! Thread ID is " << threadId << std::endl;
        std::cout << "Main Thread ID is " << ::GetCurrentThreadId() << std::endl;

        // 等待线程结束
        ::WaitForSingleObject(threadHandle, INFINITE);
    }
    else // 线程创建失败
        std::cout << "Create Thread Error!" << std::endl;

    system("pause");
    return 0;
}

函数执行结果如下
Create Thread Success! Thread ID is 162840
Main Thread ID is 9652
This is Run in Thread ID:16284, Number is :0
This is Run in Thread ID:16284, Number is :1
This is Run in Thread ID:16284, Number is :2
This is Run in Thread ID:16284, Number is :3
This is Run in Thread ID:16284, Number is :4

当然也有可能是创建的线程中先打印,后者同时交替着打印。
函数 createWindowsThread 中使用了Windows的创建线程API CreateThread , 线程中执行函数 threadProc ,while 循环中执行5次后线程退出。

  • GetCurrentThreadId 表示获取当前所在线程的 线程ID
  • WaitForSingleObject 表示等待线程结束。这里第一个参数为线程的句柄,第二个参数用于表示等待的毫秒数,如果设置为 INFINITE ,表示无限等待下去。

2.2 使用C++11创建

不同操作系统的创建线程的函数是不同的,比如windows中使用 CreateThread ,而Linux中使用 pthread_create 创建。对于跨平台的开发,我们可以使用Qt等第三方开源库完成跨平台的线程创建,也可以使用C++11中提供的方法创建多线程。当前,使用的编译器要支持C++11。

可以使用C++11提供的 std::thread 实现线程的创建。
我们改写一下上面的例子,代码如下:

#include <stdlib.h>
#include <iostream>
#include <thread>
#include <Windows.h>

// 线程中执行的函数
void threadProc(int count)
{
    bool isRunThread = true;
    while (isRunThread)
    {
        static int number = 0;
        if (number >= count)
        {
            // 仅执行Count次打印
            isRunThread = false;
            break;
        }
        std::cout << "This is Run in Thread ID:" << ::GetCurrentThreadId() << ", Number is :" << number++ << std::endl;

        Sleep(200);
    }
}

// C++11 创建线程
void createWindowsThread(void)
{
    std::thread thread(threadProc, 5);
    thread.detach();
}

int main(int argc, char** argv)
{
    createWindowsThread();

    system("pause");
    return 0;
}

程序运行结果如下
This is Run in Thread ID:10568, Number is :0
This is Run in Thread ID:10568, Number is :1
This is Run in Thread ID:10568, Number is :3
This is Run in Thread ID:10568, Number is :2
This is Run in Thread ID:10568, Number is :4

这里线程中执行的函数可以不指定为 __stdcall 修饰。创建 std::thread 对象,构造函数中传入线程中执行的函数指针和参数,如果没有参数可以不指定。就可以完成线程的创建。

  • detach 表示线程与线程对象分离,线程资源的回收不受线程对象的控制。

如果想等待线程,可使用 join 函数,我们把代码修改如下:

// C++11 创建线程
std::thread createWindowsThread(void)
{
    std::thread thread(threadProc, 5);
    return thread;
}

int main(int argc, char** argv)
{
    std::thread thread = createWindowsThread();
    if (thread.joinable())
        thread.join();

    std::cout << "Created Thread Finished!" << std::endl;

    system("pause");
    return 0;
}

执行结果如下:
This is Run in Thread ID:10568, Number is :0
This is Run in Thread ID:10568, Number is :1
This is Run in Thread ID:10568, Number is :2
This is Run in Thread ID:10568, Number is :3
This is Run in Thread ID:10568, Number is :4
Created Thread Finished!

这里因为 std::thread 实现了移动构造,所以当函数中的线程对象被消除的时候,线程的相关信息转移给了新创建的线程函数。

  • join 函数,表示阻塞等待线程的结束,如果线程结束则继续往下执行。 joinable 方法可以判断能否被 join , 在 join 前需要调用该方法。

2.3 c++11线程ID的获取

前面说了Windows的API获取线程ID的方法,那么如何使用C++11来获取线程ID呢?

  • 可以使用 std::this_thread 类的 get_id 获取当前线程的ID,这是一个静态方法。
  • 也可以使用 std::thread 对象的 get_id 获取线程ID。
    get_id 函数返回的为一个 std::thread::id 的对象, 无法直接通过该对象获取线程ID的整数值,但是该对象重载了 << 方法,如果想获取整数值需要借助 std::ostringstream 实现。 添加头文件 #include <sstream>

具体实现代码如下:

int stdThreadIdToInt(const std::thread::id& id)
{
    std::stringstream oss;
    oss << id;
    std::string string = oss.str();
    int threadId = atol(string.c_str());

    return threadId;
}
不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:使用Qt中的QThread创建线程