MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++多线程编程:线程的创建与管理

2021-03-046.3k 阅读

C++多线程编程:线程的创建与管理

1. 线程基础概念

在深入探讨C++多线程编程之前,我们先来了解一些线程的基础概念。线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等。

与传统的单线程程序相比,多线程程序能够同时执行多个任务,从而提高程序的运行效率。例如,在一个图形界面应用程序中,主线程负责处理用户界面的绘制和响应,而其他线程可以负责数据的加载、计算等任务,这样可以避免界面的卡顿,提升用户体验。

2. C++ 多线程库简介

C++ 11引入了标准库 <thread>,为C++程序员提供了一种跨平台的多线程编程方式。这个库包含了一系列与线程相关的类和函数,使得在C++中创建和管理线程变得更加容易。除了 <thread> 库之外,还有一些其他的多线程库,如POSIX线程库(pthread),它主要用于Unix - like系统;Windows API提供的线程函数,用于Windows系统。但 <thread> 库的优势在于其跨平台性,能够在不同的操作系统上使用相同的代码进行多线程编程。

3. 线程的创建

3.1 使用函数指针创建线程

在C++中,创建线程最基本的方式是使用 std::thread 类,并传入一个函数指针作为线程执行的入口点。下面是一个简单的示例:

#include <iostream>
#include <thread>

void thread_function() {
    std::cout << "This is a thread function." << std::endl;
}

int main() {
    std::thread my_thread(thread_function);
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

在上述代码中,我们定义了一个 thread_function 函数,然后在 main 函数中创建了一个线程 my_thread,并将 thread_function 作为参数传递给它。std::thread 的构造函数会启动一个新的线程,并在这个新线程中执行 thread_function 函数。注意,在 main 函数的最后,我们调用了 my_thread.join(),这一步是非常重要的。join 函数会阻塞当前线程(这里是主线程),直到被调用的线程(my_thread)执行完毕。如果不调用 join,主线程可能会在新线程还未执行完之前就结束,导致程序异常。

3.2 使用Lambda表达式创建线程

除了使用函数指针,我们还可以使用C++的Lambda表达式来创建线程。Lambda表达式提供了一种更简洁的方式来定义线程执行的函数体,特别是当这个函数体比较短小的时候。以下是示例代码:

#include <iostream>
#include <thread>

int main() {
    std::thread my_thread([]() {
        std::cout << "This is a thread created with lambda." << std::endl;
    });
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

在这个例子中,我们通过一个匿名的Lambda表达式定义了线程的执行逻辑。这种方式使得代码更加紧凑,并且不需要额外定义一个独立的函数。

3.3 传递参数给线程函数

当我们创建线程时,有时需要向线程执行的函数传递参数。对于使用函数指针创建的线程,可以在 std::thread 的构造函数中依次传递参数。示例如下:

#include <iostream>
#include <thread>

void thread_function(int num, const std::string& str) {
    std::cout << "Thread received number: " << num << " and string: " << str << std::endl;
}

int main() {
    int value = 42;
    std::string message = "Hello, thread!";
    std::thread my_thread(thread_function, value, message);
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

在上述代码中,thread_function 函数接受一个整数和一个字符串作为参数。在 main 函数中创建线程时,我们将 valuemessage 作为参数传递给 thread_function。需要注意的是,传递给线程函数的参数会被拷贝到新线程的栈中,所以即使在主线程中修改了这些参数的值,新线程中使用的仍然是拷贝的值。

对于使用Lambda表达式创建的线程,可以在Lambda表达式的捕获列表中捕获外部变量,从而在Lambda函数体中使用这些变量。例如:

#include <iostream>
#include <thread>

int main() {
    int value = 42;
    std::string message = "Hello, thread!";
    std::thread my_thread([&]() {
        std::cout << "Thread received number: " << value << " and string: " << message << std::endl;
    });
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

这里我们使用了引用捕获 [&],这样Lambda函数体中使用的 valuemessage 就是主线程中的变量,而不是拷贝。但这种方式需要注意主线程中变量的生命周期,避免在新线程使用变量时,主线程已经销毁了这些变量。

4. 线程的管理

4.1 join() 函数

join() 函数是线程管理中非常重要的一个函数,如前面例子中所示。当一个线程调用另一个线程的 join() 函数时,调用线程会阻塞,直到被调用的线程执行完毕。这确保了主线程不会在子线程完成任务之前结束。例如,在一个计算密集型的多线程程序中,主线程可能需要等待所有子线程完成计算任务后,再进行结果的汇总和处理。

#include <iostream>
#include <thread>
#include <vector>

void worker_function(int id) {
    std::cout << "Worker thread " << id << " is running." << std::endl;
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; i++) {
        threads.emplace_back(worker_function, i);
    }
    for (auto& thread : threads) {
        thread.join();
    }
    std::cout << "All worker threads have finished." << std::endl;
    return 0;
}

在这个例子中,我们创建了5个线程,并将它们存储在一个 std::vector 中。然后通过遍历这个向量,对每个线程调用 join() 函数,确保所有线程都执行完毕后,主线程才输出 "All worker threads have finished."。

4.2 detach() 函数

detach() 函数的作用与 join() 相反。当调用一个线程的 detach() 函数后,该线程就会与调用线程分离,成为一个独立的线程,不再受调用线程的控制。这个分离的线程会在后台独立运行,即使调用线程结束,它也会继续执行,直到其自身的任务完成。

#include <iostream>
#include <thread>

void background_task() {
    std::cout << "Background task is running." << std::endl;
}

int main() {
    std::thread my_thread(background_task);
    my_thread.detach();
    std::cout << "Main thread is continuing without waiting." << std::endl;
    // 这里主线程不会等待background_task执行完毕
    return 0;
}

需要注意的是,一旦线程被 detach,就无法再对其调用 join() 函数,也无法直接获取该线程的返回值(如果有的话)。并且,分离的线程所使用的资源(如栈空间)会在其结束时由系统自动回收。通常,detach() 适用于一些不需要主线程等待结果,并且执行时间较长的后台任务,如日志记录线程、数据缓存更新线程等。

4.3 std::this_thread 命名空间

std::this_thread 命名空间提供了一些与当前线程相关的实用函数。其中,std::this_thread::sleep_for 函数可以让当前线程暂停执行一段时间。这在多线程编程中非常有用,例如,当我们希望控制线程的执行节奏,避免过度占用系统资源时,可以使用这个函数。

#include <iostream>
#include <thread>
#include <chrono>

void slow_task() {
    for (int i = 0; i < 5; i++) {
        std::cout << "Slow task step " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    std::thread my_thread(slow_task);
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

slow_task 函数中,我们使用 std::this_thread::sleep_for 让线程每次循环暂停1秒钟。std::chrono::seconds(1) 表示暂停的时间为1秒,std::chrono 是C++的时间库,提供了高精度的时间表示和操作。除了 seconds,还可以使用 milliseconds(毫秒)、microseconds(微秒)等。

另外,std::this_thread::yield 函数可以让当前线程主动放弃CPU时间片,将执行权交给其他同优先级或更高优先级的线程。这在多个线程竞争CPU资源时,可以帮助实现更公平的调度。

#include <iostream>
#include <thread>

void busy_thread() {
    for (int i = 0; i < 1000000000; i++) {
        if (i % 1000000 == 0) {
            std::this_thread::yield();
        }
    }
    std::cout << "Busy thread finished." << std::endl;
}

int main() {
    std::thread my_thread(busy_thread);
    std::cout << "Main thread is running." << std::endl;
    my_thread.join();
    return 0;
}

在上述代码中,busy_thread 在每次循环到100万次时,调用 std::this_thread::yield,这样可以给其他线程一个执行的机会,避免长时间独占CPU。

5. 线程的生命周期与资源管理

线程的生命周期包括创建、运行、暂停、恢复和销毁等阶段。在C++多线程编程中,正确管理线程的生命周期和资源至关重要。

当一个线程被创建后,它会从线程函数的入口开始执行。在线程执行过程中,如果遇到 std::this_thread::sleep_for 等函数,线程会进入暂停状态,直到指定的时间过去或被其他线程唤醒。如果线程调用了 join() 函数,它会等待另一个线程执行完毕,这期间也可以看作是一种暂停状态。

在资源管理方面,由于多个线程共享进程的资源,如内存、文件描述符等,可能会出现资源竞争的问题。例如,两个线程同时尝试写入同一个文件,可能会导致文件内容混乱。为了避免这种情况,我们需要使用同步机制,如互斥锁(Mutex)、信号量(Semaphore)等,这将在后续的文章中详细介绍。

另外,线程的栈空间也是一种资源。每个线程都有自己独立的栈空间,用于存储局部变量、函数调用栈等。栈空间的大小在不同的操作系统和编译器下可能会有所不同。在编写多线程程序时,要注意避免栈溢出的问题,特别是在线程函数中递归调用或者定义大量的局部变量时。

6. 线程创建与管理中的常见问题与解决方法

6.1 线程泄漏

线程泄漏是指在程序中创建了线程,但没有正确地回收线程资源,导致线程一直存在,占用系统资源。通常,这是由于没有调用 join()detach() 函数引起的。例如:

#include <iostream>
#include <thread>

void leaky_thread() {
    std::thread my_thread([]() {
        std::cout << "Leaky thread is running." << std::endl;
    });
    // 这里没有调用join()或detach()
}

int main() {
    leaky_thread();
    std::cout << "Main thread is running." << std::endl;
    return 0;
}

leaky_thread 函数中,创建了一个线程,但没有对其进行任何管理。当 leaky_thread 函数返回时,这个线程仍然在运行,但其资源无法被正确回收。为了解决这个问题,必须在合适的地方调用 join()detach() 函数。如果需要等待线程完成任务,可以调用 join();如果希望线程在后台独立运行,可以调用 detach()

6.2 异常处理

在多线程程序中,异常处理也是一个重要的问题。当线程函数中抛出异常时,如果没有正确处理,可能会导致程序崩溃或资源泄漏。例如:

#include <iostream>
#include <thread>
#include <stdexcept>

void thread_with_exception() {
    throw std::runtime_error("Thread exception occurred.");
}

int main() {
    std::thread my_thread(thread_with_exception);
    try {
        my_thread.join();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在这个例子中,thread_with_exception 函数抛出了一个 std::runtime_error 异常。在 main 函数中,我们通过 try - catch 块捕获了这个异常,并进行了相应的处理。如果没有这个 try - catch 块,异常可能会导致程序终止,并且线程资源可能无法正确释放。

6.3 线程优先级

在一些情况下,我们可能需要调整线程的优先级,以确保某些重要的线程能够优先获得CPU资源。在C++标准库中,并没有直接提供设置线程优先级的功能,但可以通过操作系统相关的API来实现。例如,在Windows系统中,可以使用 SetThreadPriority 函数;在Unix - like系统中,可以使用 pthread_setschedparam 函数。以下是一个在Windows系统中设置线程优先级的示例:

#include <iostream>
#include <thread>
#include <windows.h>

void high_priority_task() {
    std::cout << "High priority task is running." << std::endl;
}

int main() {
    std::thread high_priority_thread(high_priority_task);
    HANDLE handle = high_priority_thread.native_handle();
    SetThreadPriority(handle, THREAD_PRIORITY_HIGHEST);
    std::cout << "Main thread is running." << std::endl;
    high_priority_thread.join();
    return 0;
}

在上述代码中,我们通过 std::thread::native_handle 获取线程的原生句柄,然后使用 SetThreadPriority 函数将线程的优先级设置为最高。需要注意的是,不同操作系统设置线程优先级的方式和优先级级别有所不同,在实际应用中要根据具体的操作系统进行调整。

7. 跨平台多线程编程注意事项

虽然C++标准库 <thread> 提供了跨平台的多线程编程接口,但在实际开发中,仍然有一些需要注意的地方。

不同操作系统对线程的支持和实现细节存在差异。例如,线程的默认栈大小、线程调度算法等可能不同。在编写跨平台多线程程序时,要充分考虑这些差异,避免依赖特定操作系统的行为。

另外,一些操作系统特定的功能可能无法通过C++标准库直接访问。如前面提到的设置线程优先级,需要使用操作系统相关的API。在这种情况下,如果要实现跨平台,可能需要编写条件编译代码,根据不同的操作系统选择不同的实现方式。

#ifdef _WIN32
#include <windows.h>
#elif defined(__linux__)
#include <pthread.h>
#include <sched.h>
#endif

#include <iostream>
#include <thread>

void set_high_priority(std::thread& thread) {
#ifdef _WIN32
    HANDLE handle = thread.native_handle();
    SetThreadPriority(handle, THREAD_PRIORITY_HIGHEST);
#elif defined(__linux__)
    pthread_t pthread = thread.native_handle();
    struct sched_param param;
    param.sched_priority = sched_get_priority_max(SCHED_RR);
    pthread_setschedparam(pthread, SCHED_RR, &param);
#endif
}

void high_priority_task() {
    std::cout << "High priority task is running." << std::endl;
}

int main() {
    std::thread high_priority_thread(high_priority_task);
    set_high_priority(high_priority_thread);
    std::cout << "Main thread is running." << std::endl;
    high_priority_thread.join();
    return 0;
}

在上述代码中,我们通过条件编译,根据不同的操作系统(Windows和Linux)实现了设置线程优先级的功能。这样可以在不同操作系统上保持代码的一致性和可移植性。

同时,在跨平台开发中,还要注意不同操作系统对字符编码、文件路径格式等方面的差异,这些也可能会影响多线程程序的正确性和稳定性。

通过对C++多线程编程中线程的创建与管理的深入探讨,我们了解了如何使用C++标准库创建线程、传递参数、管理线程的生命周期以及处理常见问题。在实际应用中,多线程编程可以显著提高程序的性能和响应性,但也需要谨慎处理资源竞争、异常等问题,以确保程序的稳定性和正确性。