学习c++的第十六天

目录

多线程

多进程与多线程

多线程理解

创建线程

joinable方法

this_thread

mutex

lock与unlock

lock_guard

unique_lock

condition_variable

wait

wait_for

线程池

概念

线程池的实现


多线程

C++11标准的引入为多线程编程带来了很大的变化和便利。在C++11之前,要实现多线程编程通常需要借助操作系统提供的线程库,比如POSIX 线程库()或 Windows 线程库()。这些库对于不同的操作系统有着不同的实现和接口,因此跨平台开发时需要针对不同的操作系统进行适配,增加了开发的复杂性。

而在C++11标准中,新增了 头文件,引入了一组标准化的多线程支持库,使得多线程编程变得更加简单和易于移植。C++11标准引入的与多线程相关的头文件包括:

  • :用于创建和管理线程。
  • :提供了互斥量(mutex)类,用于保护共享数据,防止多个线程同时访问造成的数据竞争。
  • :提供了条件变量(condition variable)类,用于线程间的同步操作。
  • :提供了原子操作的支持,可以确保在多线程环境下对共享变量的操作是原子的。
  • :提供了异步(asynchronous)操作的支持,允许在一个线程中异步执行一个函数,并在另一个线程中获取函数的返回值。

这些头文件的引入使得C++语言本身具备了丰富的多线程编程支持,开发者可以使用标准化的接口来进行多线程编程,而不需要直接依赖于特定操作系统的线程库,从而实现了更好的跨平台性和可移植性。

多进程与多线程

多进程并发

优点:

  1. 进程之间相互独立,一个进程崩溃不会影响其他进程,稳定性高。
  2. 操作系统提供了大量的保护机制,容易编写安全的代码。

缺点:

  1. 进程间通信复杂,速度较慢且开销较大。
  2. 运行多个进程的开销很大,操作系统需要分配大量资源进行管理。

多线程并发

优点:

  1. 线程是轻量级的,创建和切换的开销相对较小。
  2. 多个线程共享相同的地址空间,方便进行数据共享和通信。

缺点:

  1. 缺少操作系统提供的保护机制,需要程序员编写更多的代码来保证数据操作的正确性,避免死锁等问题。

在实际应用中,选择多进程并发还是多线程并发,需要考虑任务的性质、并发访问共享资源的情况以及系统的性能需求。例如,对于需要进行独立隔离任务的场景,多进程可能更适合;而对于需要并发处理、资源共享的任务,多线程可能更适合。同时,需要注意合理处理并发访问共享资源的问题,避免出现数据竞争和死锁等并发编程常见问题。

多线程理解

在单CPU内核的情况下,即使有多个线程存在,由于只有一个物理处理器核心,实际上每个线程都只能依次执行,即通过时间片轮转的方式来快速切换线程的执行。这种情况下,虽然看起来多个任务几乎同时在运行,但实际上是通过快速切换实现的,并不能真正达到并行计算的效果。

而在多个CPU或者多个内核的情况下,每个CPU核心都能够独立地执行指令,因此多个线程可以被同时分配到不同的CPU核心上并行执行,从而实现真正意义上的并行计算。这样可以显著提高程序的整体执行速度和性能。

在进行并行计算时,多核CPU系统通常可以更好地利用硬件资源,加快计算速度。当然,在编写并行程序时,也需要考虑到数据共享、同步和通信等问题,以充分发挥多核并行计算的优势。

创建线程

1、std::thread myThread(thread_fun); 创建了一个线程对象 myThread,并将函数 thread_fun 作为线程的执行体。然后通过 myThread.join(); 等待该线程执行结束。

2、std::thread myThread(thread_fun, 100); 创建了一个线程对象 myThread,并将带有参数的函数 thread_fun 和参数 100 作为线程的执行体。同样地,通过 myThread.join(); 等待该线程执行结束。

3、std::thread(thread_fun, 1).detach(); 直接创建了一个线程,并调用 detach 方法,使得线程对象脱离主线程的控制。这意味着该线程可以在后台独立执行,而不需要等待主线程结束。

以上三种形式都展示了如何使用 std::thread 来创建线程,并且可以通过传递不同的参数以及不同的函数形式来实现代码的复用和多线程的管理。

#include 
#include 

// 形式1:无参数的线程函数
void thread_fun() {
    std::cout << "这是一个没有参数的线程。" << std::endl;
}

// 形式2:带参数的线程函数
void thread_fun_with_param(int x) {
    std::cout << "这是一个带有参数的线程: " << x << std::endl;
}

int main() {
    // 形式1:使用无参数函数创建线程
    std::thread myThread1(thread_fun);
    myThread1.join();

    // 形式2:使用带参数的函数创建线程
    std::thread myThread2(thread_fun_with_param, 100);
    myThread2.join();

    // 形式3:使用 lambda 表达式创建线程
    std::thread([](int x) {
        std::cout << "这是一个使用lambda表达式创建的线程: " << x << std::endl;
        }, 200).detach();

        return 0;
}

在上面的示例中,我们定义了两个线程函数 thread_fun 和 thread_fun_with_param,并且演示了三种不同的形式来创建和启动线程。

在 main 函数中,我们首先使用无参数函数创建了一个线程 myThread1,然后使用带参数的函数创建了另一个线程 myThread2。最后使用 lambda 表达式创建了一个脱离主线程控制的线程,由于我们使用了 detach 方法将其脱离了主线程的控制,因此它的输出可能会在主线程的输出之后,也可能会在主线程的输出之前,这取决于系统调度线程的顺序。因此,它的输出顺序可能是不确定的。

当你运行这段代码时,你会看到这三个线程分别输出它们的内容,展示了不同形式下线程的创建和执行情况。

joinable方法

在C++中,可以使用joinable方法来确定线程是否可以被join或detach。

  • 如果一个std::thread对象关联的线程可以被join,即这个线程已经启动但尚未被join或detach,那么joinable方法会返回true。
  • 如果一个std::thread对象已经与某个线程分离(被detach),或者它从未与任何线程相关联,那么joinable方法会返回false。

通过检查joinable方法的返回值,可以确定线程是处于可被join的状态,还是已经被detach了。

在实际编程中,可以在启动线程之后立即检查joinable的返回值,以确定需要采取哪种等待线程执行结束的方式。这样可以避免在不适当的时候调用join或detach而导致程序出现未定义行为。

代码举例

当使用joinable函数时,我们通常会在创建线程后立即检查线程是否可被join,以便根据需要选择合适的处理方式。以下是一个简单的示例:

#include 
#include 

void threadFunction() {
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "线程函数已执行" << std::endl;
}

int main() {
    std::thread myThread(threadFunction);

    // 检查线程是否可被join
    if (myThread.joinable()) {
        std::cout << "线程是可连接的,正在等待线程完成" << std::endl;
        myThread.join();  // 等待线程执行结束
    }
    else {
        std::cout << "线程不可连接,正在分离线程" << std::endl;
        myThread.detach();  // 将线程分离
    }

    std::cout << "执行的主要功能" << std::endl;

    return 0;
}

在这个例子中,首先创建了一个名为myThread的线程,并在主函数中立即检查它是否可被join。如果线程可被join,则调用join来等待线程执行结束;如果线程不可被join,则将其分离。根据线程的处理状态,程序会输出不同的信息。 

this_thread

在C++11标准中,std::this_thread提供了一组函数来处理与当前线程相关的操作。

  • get_id: 用于获取当前线程的ID。
  • yield: 放弃当前线程的执行,使调度器可以选择其他就绪状态的线程来执行。
  • sleep_for: 使当前线程在指定的时间段内休眠。
  • sleep_until: 使当前线程在指定的时间点之前一直休眠,直到指定的时间点到来。

这些函数可以帮助我们管理线程的执行和调度,以及控制线程的等待和休眠时间。对于编写高效、可靠的多线程程序来说,这些函数是非常有用的工具。

代码举例

当使用std::this_thread的功能函数时,我们可以通过具体的代码示例来演示它们的使用方法。以下是一个简单的C++代码示例,展示了std::this_thread的几个功能函数的用法:

#include 
#include 
#include 

void threadFunction() {
    std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "睡眠后执行线程" << std::endl;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;

    std::thread myThread(threadFunction);
    myThread.join();

    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "睡眠后执行主线程" << std::endl;

    return 0;
}

在这个例子中,我们首先在主线程和子线程中使用std::this_thread::get_id()来获取线程ID,并在控制台输出。然后,在子线程中使用std::this_thread::sleep_for()使线程休眠1秒。在主线程中也使用std::this_thread::sleep_for()使线程休眠2秒。通过这个例子,我们可以清晰地看到这些功能函数的作用。

mutex

C++的头文件提供了几种不同类型的互斥量,以满足不同情况下的需求。这些类型包括:

  • std::mutex:最基本的互斥量类型,提供独占所有权的特性,即不支持递归地对其上锁。
  • std::recursive_mutex:递归互斥量类型,允许同一线程多次对互斥量上锁而不会造成死锁。
  • std::timed_mutex:定时互斥量类型,提供了尝试获取锁并在超时之后放弃的能力。
  • std::recursive_timed_mutex:定时递归互斥量类型,结合了递归和定时的特性。

选择合适的互斥量类型取决于具体的应用场景和需求。例如,如果需要支持同一线程多次对互斥量上锁,可以选择std::recursive_mutex;如果需要对互斥量的锁定操作设置超时机制,可以选择std::timed_mutex。

另外,需要注意的是,在使用这些互斥量时,要遵循良好的编程实践,避免死锁等问题的发生。同时,对于不同的互斥量类型,也需要仔细考虑其在多线程环境中的行为,以确保程序的正确性和性能。

lock与unlock

在C++的头文件中,std::mutex类提供了lock()和unlock()成员函数,用于手动上锁和解锁互斥量。而try_lock()则是一种非阻塞的尝试上锁操作。在实际编程中,这些操作可以有效地帮助我们管理共享资源的访问,避免数据竞争和死锁等问题。

1、lock()

  • lock()函数用于手动上锁互斥量,如果当前互斥量已经被其他线程所占有,则调用lock()的线程会被阻塞,直到获得互斥量的所有权。
  • 当使用lock()函数时,如果当前线程已经拥有了互斥量的所有权,它将会产生死锁。因此,需要谨慎使用lock()函数,特别是在涉及到递归锁的情况下。

2、unlock()

  • unlock()函数用于手动解锁互斥量,将互斥量的所有权释放,使得其他线程可以继续竞争互斥量的所有权。
  • 如果在没有拥有互斥量所有权的情况下调用unlock()函数,或者对已解锁的互斥量再次调用unlock(),会导致未定义行为。

关于try_lock()函数,它有三种情况:

  • 未上锁返回false,并锁住;
  • 其他线程已经上锁,返回true;
  • 同一个线程已经对它上锁,将会产生死锁。

对于第三种情况,要澄清一点:try_lock()函数不会直接导致死锁。如果同一个线程已经对互斥量上锁,再次调用try_lock()时,会返回false,表示未能成功上锁。而真正的死锁是指多个线程相互等待对方释放资源而无法继续执行的情况,与try_lock()函数的行为并不直接相关。

举例来说,假设有两个线程 A 和 B,它们分别尝试对两个互斥量 M1 和 M2 进行上锁。如果 A 已经锁住了 M1,而 B 已经锁住了 M2,然后它们又都尝试对另一个互斥量进行上锁,这时就会发生死锁。但是单独使用try_lock()函数不会自动导致死锁,因为它只是在尝试上锁时返回成功或失败的结果而已。

总的来说,try_lock()函数是一个非阻塞的尝试上锁操作,可以用于避免阻塞等待互斥量的情况,但在实际使用时需要注意处理返回结果,并结合程序逻辑来避免潜在的死锁问题。

代码举例

当使用 C++ 的标准库时,可以使用 std::mutex 和相关的成员函数来展示 lock()、unlock() 和 try_lock() 的用法。下面是一个简单的示例代码,演示了如何使用这些函数来对共享资源进行加锁和解锁操作:

#include 
#include 
#include 

std::mutex mtx; // 定义一个互斥量作为全局变量

void sharedResourceAccess(int id) {
    // 尝试上锁
    if (mtx.try_lock()) {
        std::cout << "线程 " << id << " 已锁定互斥对象。" << std::endl;

        // 模拟对共享资源的访问
        std::this_thread::sleep_for(std::chrono::seconds(1));

        mtx.unlock(); // 解锁
        std::cout << "线程 " << id << " 已解锁互斥对象。" << std::endl;
    }
    else {
        std::cout << "线程 " << id << " 无法锁定互斥对象." << std::endl;
    }
}

int main() {
    std::thread t1(sharedResourceAccess, 1);
    std::thread t2(sharedResourceAccess, 2);

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

    return 0;
}

在这个示例中,我们创建了两个线程 t1 和 t2,它们都调用 sharedResourceAccess 函数来尝试对互斥量 mtx 进行上锁。如果其中一个线程成功上锁了互斥量,它会访问共享资源并在一段时间后释放互斥量;如果上锁失败,则会打印相应的提示信息。这个示例演示了如何使用try_lock()来进行非阻塞的上锁尝试,以及如何在多线程环境下控制对共享资源的访问。

lock_guard

创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

std::lock_guard 具有这些特点:

  • 创建即加锁:在构造std::lock_guard对象时,会立即尝试获取提供给它的互斥锁的所有权,即上锁操作。
  • 作用域结束自动析构并解锁:当控制流离开std::lock_guard对象的作用域时(比如离开当前代码块),std::lock_guard对象会被销毁,从而自动释放互斥量,即解锁操作。
  • 无需手工解锁:使用std::lock_guard可以避免手工管理互斥量的上锁和解锁操作,大大简化了代码的编写和维护。
  • 不能中途解锁:确实,std::lock_guard不支持中途手动解锁互斥量,只能在作用域结束时自动解锁。
  • 不能复制:由于 std::lock_guard 的设计初衷是为了确保资源的独占性,因此它是不可复制的,这也避免了资源竞争和错误的发生。

这些特点使得std::lock_guard成为一种非常便捷和安全的管理互斥量的方式,可以有效地帮助我们编写出更加健壮和可靠的多线程程序。

代码举例

下面是一个示例代码,演示了如何使用std::lock_guard来管理互斥量的上锁和解锁操作:

#include 
#include 
#include 

std::mutex mtx; // 定义一个互斥量作为全局变量

void sharedResourceAccess(int id) {
    std::lock_guard lock(mtx); // 在此处上锁

    std::cout << "线程 " << id << " 已锁定互斥对象." << std::endl;

    // 模拟对共享资源的访问
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 在此处自动解锁
    std::cout << "线程 " << id << " 已解锁互斥对象。" << std::endl;
}

int main() {
    std::thread t1(sharedResourceAccess, 1);
    std::thread t2(sharedResourceAccess, 2);

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

    return 0;
}

在这个示例中,我们使用std::lock_guard来创建了一个名为lock的对象,它在构造时会自动上锁互斥量mtx,而在析构时会自动解锁。这样,当线程执行完毕离开了sharedResourceAccess函数时,std::lock_guard对象lock会被销毁,从而自动释放互斥量,确保了对共享资源的安全访问。

unique_lock

std::unique_lock 与 std::lock_guard 类似,也用于管理互斥量的上锁和解锁操作,但是相较于 std::lock_guard,它是 std::lock_guard 的升级版本,提供了更多的灵活性和功能,适用于更复杂的多线程场景。它具有以下特点:

  • 可以手动上锁和解锁:std::unique_lock 具有成员函数 lock() 和 unlock(),允许在需要的时候手动控制互斥量的上锁和解锁。
  • 支持延迟上锁:可以在构造 std::unique_lock 对象时选择是否立即上锁,或者延迟到稍后的时间点再上锁。
  • 可以转移所有权:std::unique_lock 对象可以转移对互斥量的所有权,这意味着一个 std::unique_lock 对象可以接管另一个 std::unique_lock 对象所管理的互斥量。
  • 支持条件变量:std::unique_lock 可以与条件变量一起使用,通过成员函数 wait() 和 notify_one() 或 notify_all() 实现线程间的协调与同步。

在实际使用中,如果你需要更灵活地控制锁的上锁和解锁操作,或者需要支持条件变量,那么 std::unique_lock 是一个更好的选择。而如果你只需要简单地保护一个临界区,可以考虑使用 std::lock_guard,因为它更简单直接,代码更加清晰易懂。

以下是一个使用 std::unique_lock 的示例代码:

#include 
#include 
#include 

std::mutex mtx; // 定义一个互斥量作为全局变量
std::condition_variable cv; // 定义条件变量

void sharedResourceAccess(int id) {
    std::unique_lock lock(mtx); // 在此处上锁

    std::cout << "线程 " << id << " 已锁定互斥对象。" << std::endl;

    // 模拟对共享资源的访问
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 在此处自动解锁
    std::cout << "线程 " << id << " 已解锁互斥对象。" << std::endl;
}

int main() {
    std::thread t1(sharedResourceAccess, 1);
    std::thread t2(sharedResourceAccess, 2);

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

    return 0;
}

condition_variable

condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。

condition_variable和condition_variable_any都是C++标准库中用于多线程同步的条件变量类型,它们的作用是在多个线程协作时进行同步与通信。

condition_variable必须配合std::unique_lock,std::mutex使用,而condition_variable_any则可以和任何类型的锁一起使用。它们的基本用法和功能包括:

1、condition_variable

  • 构造函数:用于创建condition_variable对象。
  • 析构函数:用于销毁condition_variable对象。
  • wait:使当前线程在条件变量上等待,同时释放与std::unique_lockstd::mutex关联的锁,直到收到通知。
  • wait_for:在经过指定时间或者收到通知之前,使当前线程在条件变量上等待。
  • wait_until:在指定的时间点之前或者收到通知之前,使当前线程在条件变量上等待。
  • notify_one:通知等待在条件变量上的一个线程,使其从wait中返回,继续执行。
  • notify_all:通知所有等待在条件变量上的线程,使它们从wait中返回,继续执行。
  • cv_status:这是一个枚举类型,表示condition_variable_wait的状态,可以是no_timeout(未超时)或timeout(等待超时)。

2、condition_variable_any

  • 与condition_variable类似,但可以与任何类型的锁一起使用。

通过使用这些功能,可以实现线程之间的有效通信与同步。在多线程编程中,条件变量是一种重要的同步机制,能够帮助线程等待特定条件的发生,以及在条件满足时进行通知和唤醒。

wait

在调用wait()后,当前线程会被阻塞,此时假设当前线程已经获得了锁(mutex),然后等待其他线程调用notify_*来唤醒它。

在线程被阻塞时,wait()函数会自动调用lck.unlock()释放锁,这样其他被阻塞在锁竞争上的线程可以继续执行。一旦当前线程收到通知(notified,通常是另外某个线程调用notify_*),wait()函数也会自动调用lck.lock(),以使得锁的状态和wait函数被调用时相同。

代码示例:

我们可以使用C++的标准库中的条件变量(condition variable)来演示wait()和notify()机制。以下是一个简单的示例,展示了这两个函数在多线程编程中的基本用法:

#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker_thread() {
    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::unique_lock lock(mtx);
    ready = true;
    std::cout << "Worker: 数据准备完毕\n";
    lock.unlock();
    cv.notify_one();  // 唤醒等待的线程
}

int main() {
    std::cout << "主线程正在等待数据准备...\n";
    std::thread worker(worker_thread);

    std::unique_lock lock(mtx);
    cv.wait(lock, [] { return ready; });
    std::cout << "主线程: 收到通知,数据已经准备好\n";

    worker.join();
    return 0;
}

在这个示例中,主线程在调用cv.wait()时会被阻塞,直到worker线程中的cv.notify_one()唤醒它。这个示例展示了wait()和notify()机制在实际多线程情境中的使用。

wait_for

wait_for是C++标准库中条件变量(std::condition_variable)的一个成员函数,它允许线程等待一段时间直到满足特定条件或者超时。

在使用wait_for时,我们需要传入一个互斥锁(std::unique_lock)和一个时间段(std::chrono::duration),以及一个可调用对象(predicate)来表示等待的条件。wait_for的具体调用方式为:

template< class Rep, class Period, class Predicate >
cv_status wait_for( std::unique_lock& lock, const std::chrono::duration& rel_time, Predicate pred );

其中:

  • lock是一个已经加锁的互斥锁。
  • rel_time是一个表示相对时间段的std::chrono::duration类型参数,用来指定等待的时间长度。
  • pred是一个可调用对象,用于判断等待的条件是否满足。

wait_for函数的作用是,当满足以下任一条件之一时,函数返回:

  • 等待时间到达(超时)。
  • 条件满足(即pred返回true)。

当使用std::condition_variable::wait_for时,通常需要结合std::unique_lock和std::chrono来实现线程的等待和超时检测。下面是一个简单的示例,演示了如何在C++中使用wait_for来实现线程的等待和超时检测。

#include 
#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;

void worker_thread() {
    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    {
        std::lock_guard lock(mtx);
        data_ready = true;
    }
    cv.notify_one(); // 唤醒等待的线程
}

int main() {
    std::cout << "主线程正在等待数据准备...\n";
    std::thread worker(worker_thread);

    {
        std::unique_lock lock(mtx);
        if (cv.wait_for(lock, std::chrono::seconds(1), [] { return data_ready; })) {
            std::cout << "主线程: 收到通知,数据已经准备好\n";
        }
        else {
            std::cout << "主线程: 等待超时,数据还未准备好\n";
        }
    }

    worker.join();
    return 0;
}

在这个示例中,主线程先创建了一个工作线程worker_thread,然后主线程通过cv.wait_for等待1秒钟。如果在这段时间内data_ready变为true,就表示收到了通知;否则,等待超时。在另一个线程中,worker_thread函数模拟了一些工作,并在工作完成后设置了data_ready为true,并通过cv.notify_one()来唤醒等待的线程。

这个示例展示了wait_for的基本用法,通过使用互斥锁、条件变量和时间段,实现了线程之间的等待和超时检测。

线程池

概念

线程池的概念正是为了应对你提到的这些问题而被引入的。通过维护一组预先创建好的线程,线程池可以避免频繁地创建和销毁线程所带来的开销,提高程序的性能和资源利用率。

在一个程序中重复使用线程时,线程池能够解决如下问题:

1. 资源利用率:由于线程已经被创建并初始化,它们可以立即投入工作,避免了因线程创建和初始化所带来的延迟和开销,从而提高了资源的利用率。

2. 响应速度:由于线程已经准备就绪,可以立即执行任务,不需要等待线程创建和初始化,因此可以更快地响应任务的到来。

3. 系统开销:通过控制线程的数量和生命周期,线程池可以减少系统中线程的总数,从而降低了系统开销,避免了线程过多带来的资源竞争和调度开销。

4. 资源饥饿:通过合理的线程管理,线程池可以避免线程销毁过慢导致的资源饥饿现象,保证资源的公平分配和高效利用。

综上所述,线程池的引入能够显著改善程序在多线程场景下的性能表现,提高资源利用率并降低系统开销。因此,在需要频繁使用线程的应用中,使用线程池是一种有效的优化手段。

线程池的实现

线程池的实现通常包括以下几个部分:

  • 线程池管理器(ThreadPoolManager):线程池管理器负责创建并管理线程池,它可能包含线程池的初始化、线程的添加和移除、线程池的关闭等功能。在Java中,可以使用ExecutorService接口或其实现类来作为线程池管理器。
  • 工作线程(WorkThread):工作线程是线程池中的线程,它们负责执行从任务队列中获取的任务。工作线程的数量和行为由线程池管理器控制。
  • 任务队列(Task Queue):任务队列用于存放尚未被执行的任务,提供了一种缓冲机制,让线程池中的工作线程可以按需获取任务并执行。任务队列可以是有界队列或无界队列,具体选择取决于业务需求。
  • 任务添加接口(Append):这是一个用于向线程池提交新任务的接口,通过调用这个接口,可以将待处理的任务添加到任务队列中,以便线程池中的工作线程能够执行这些任务。

根据上述构成部分,可以通过以下步骤来实现简单的线程池:

  • 创建一个线程池管理器(ThreadPoolManager),负责线程池的创建和管理。
  • 在线程池管理器中创建工作线程(WorkThread),并启动这些工作线程。工作线程会从任务队列中取出任务并执行。
  • 定义一个任务队列(Task Queue),用于存放需要执行的任务。
  • 提供任务添加接口(Append),用于向任务队列中添加新的任务。

线程池实现代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

class ThreadPool {
public:
    ThreadPool(size_t numThreads) : stop(false) {
        // 创建指定数量的工作线程
        for (size_t i = 0; i < numThreads; ++i) {
            workers.emplace_back(
                [this] {
                    while (true) {
                        std::function task;
                        {
                            // 对任务队列使用互斥锁保护
                            std::unique_lock lock(this->queue_mutex);
                            // 使用条件变量等待任务或线程池停止信号
                            this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); });
                            // 如果线程池停止并且任务队列为空,则线程退出
                            if (this->stop && this->tasks.empty()) {
                                return;
                            }
                            // 从任务队列中取出任务
                            task = std::move(this->tasks.front());
                            this->tasks.pop();
                        }
                        // 执行任务
                        task();
                    }
                }
            );
        }
    }

    // 将任务添加到任务队列
    template
    void enqueue(F&& f) {
        {
            // 对任务队列使用互斥锁保护
            std::unique_lock lock(queue_mutex);
            tasks.emplace(std::forward(f));
        }
        // 通知一个工作线程有新的任务可以执行
        condition.notify_one();
    }

    // 销毁线程池
    ~ThreadPool() {
        {
            // 对任务队列使用互斥锁保护
            std::unique_lock lock(queue_mutex);
            stop = true;
        }
        // 通知所有工作线程线程池即将销毁
        condition.notify_all();
        for (std::thread &worker : workers) {
            worker.join();
        }
    }

private:
    std::vector workers; // 工作线程
    std::queue> tasks; // 任务队列

    std::mutex queue_mutex; // 互斥锁
    std::condition_variable condition; // 条件变量
    bool stop; // 线程池停止信号
};

int main() {
    ThreadPool pool(4); // 创建包含4个工作线程的线程池实例

    // 向线程池中添加8个任务
    for (int i = 0; i < 8; ++i) {
        pool.enqueue([i] {
            std::cout << "任务 " << i << " 执行" << std::endl;
        });
    }

    return 0;
}

这段代码创建了一个名为ThreadPool的类来实现线程池功能。在主函数main中,创建了一个包含4个线程的线程池实例pool,并向线程池中添加了8个任务。每个任务都是一个lambda表达式,在执行时输出相应的信息。

ThreadPool类包括enqueue方法用于将任务添加到任务队列中,以及构造函数和析构函数用于创建和销毁线程池。在构造函数中,会创建指定数量的线程,并且每个线程都会不断地从任务队列中取出任务并执行,直到线程池被销毁。同时,使用了互斥锁和条件变量来实现线程间的同步操作,保证线程安全地执行任务队列中的任务。

你可能感兴趣的:(C++,学习,c++,开发语言)