目录
多线程
多进程与多线程
多线程理解
创建线程
joinable方法
this_thread
mutex
lock与unlock
lock_guard
unique_lock
condition_variable
wait
wait_for
线程池
概念
线程池的实现
C++11标准的引入为多线程编程带来了很大的变化和便利。在C++11之前,要实现多线程编程通常需要借助操作系统提供的线程库,比如POSIX 线程库(
而在C++11标准中,新增了
这些头文件的引入使得C++语言本身具备了丰富的多线程编程支持,开发者可以使用标准化的接口来进行多线程编程,而不需要直接依赖于特定操作系统的线程库,从而实现了更好的跨平台性和可移植性。
多进程并发
优点:
缺点:
多线程并发
优点:
缺点:
在实际应用中,选择多进程并发还是多线程并发,需要考虑任务的性质、并发访问共享资源的情况以及系统的性能需求。例如,对于需要进行独立隔离任务的场景,多进程可能更适合;而对于需要并发处理、资源共享的任务,多线程可能更适合。同时,需要注意合理处理并发访问共享资源的问题,避免出现数据竞争和死锁等并发编程常见问题。
在单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 方法将其脱离了主线程的控制,因此它的输出可能会在主线程的输出之后,也可能会在主线程的输出之前,这取决于系统调度线程的顺序。因此,它的输出顺序可能是不确定的。
当你运行这段代码时,你会看到这三个线程分别输出它们的内容,展示了不同形式下线程的创建和执行情况。
在C++中,可以使用joinable方法来确定线程是否可以被join或detach。
通过检查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,则将其分离。根据线程的处理状态,程序会输出不同的信息。
在C++11标准中,std::this_thread提供了一组函数来处理与当前线程相关的操作。
这些函数可以帮助我们管理线程的执行和调度,以及控制线程的等待和休眠时间。对于编写高效、可靠的多线程程序来说,这些函数是非常有用的工具。
代码举例
当使用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秒。通过这个例子,我们可以清晰地看到这些功能函数的作用。
C++的
选择合适的互斥量类型取决于具体的应用场景和需求。例如,如果需要支持同一线程多次对互斥量上锁,可以选择std::recursive_mutex;如果需要对互斥量的锁定操作设置超时机制,可以选择std::timed_mutex。
另外,需要注意的是,在使用这些互斥量时,要遵循良好的编程实践,避免死锁等问题的发生。同时,对于不同的互斥量类型,也需要仔细考虑其在多线程环境中的行为,以确保程序的正确性和性能。
在C++的
1、lock()
2、unlock()
关于try_lock()函数,它有三种情况:
对于第三种情况,要澄清一点: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析构并释放互斥量。
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会被销毁,从而自动释放互斥量,确保了对共享资源的安全访问。
std::unique_lock 与 std::lock_guard 类似,也用于管理互斥量的上锁和解锁操作,但是相较于 std::lock_guard,它是 std::lock_guard 的升级版本,提供了更多的灵活性和功能,适用于更复杂的多线程场景。它具有以下特点:
在实际使用中,如果你需要更灵活地控制锁的上锁和解锁操作,或者需要支持条件变量,那么 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的头文件有两个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
2、condition_variable_any
通过使用这些功能,可以实现线程之间的有效通信与同步。在多线程编程中,条件变量是一种重要的同步机制,能够帮助线程等待特定条件的发生,以及在条件满足时进行通知和唤醒。
在调用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是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 );
其中:
wait_for函数的作用是,当满足以下任一条件之一时,函数返回:
当使用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. 资源饥饿:通过合理的线程管理,线程池可以避免线程销毁过慢导致的资源饥饿现象,保证资源的公平分配和高效利用。
综上所述,线程池的引入能够显著改善程序在多线程场景下的性能表现,提高资源利用率并降低系统开销。因此,在需要频繁使用线程的应用中,使用线程池是一种有效的优化手段。
线程池的实现通常包括以下几个部分:
根据上述构成部分,可以通过以下步骤来实现简单的线程池:
线程池实现代码:
#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方法用于将任务添加到任务队列中,以及构造函数和析构函数用于创建和销毁线程池。在构造函数中,会创建指定数量的线程,并且每个线程都会不断地从任务队列中取出任务并执行,直到线程池被销毁。同时,使用了互斥锁和条件变量来实现线程间的同步操作,保证线程安全地执行任务队列中的任务。