代码随想录八股训练营第四十天| C++
目录
一、什么是菱形继承?
1.1.菱形继承的示例:
1.2.菱形继承的问题:
1.3.解决菱形继承问题:
二、C++中的多线程同步机制?
2.1.互斥锁(Mutex):
2.2.递归互斥锁(Recursive Mutex):
2.3.读写锁(Read-Write Lock):
2.4.条件变量(Condition Variable):
2.5.原子操作(Atomic Operations):
2.6.屏障(Barrier):
2.7.信号量(Semaphore):
2.8.纤程(Fiber):
三、如何在c++中创建和管理线程?
3.1.包含必要的头文件:
3.2.创建线程:
3.3.等待线程结束:
3.4.分离线程:
3.5.使用线程局部存储:
3.6.线程同步:
3.7.线程同步:
3.8.线程同步:
3.9.使用 C++20 协程:
总结
前言
在现代软件开发中,多线程编程和继承结构的合理设计是提高程序性能和代码复用性的关键。C++ 作为一种功能强大的编程语言,提供了丰富的特性来支持多线程编程和复杂的继承模式。本文档将详细介绍 C++ 中的菱形继承问题、多线程同步机制,以及如何在 C++ 中创建和管理线程。
一、什么是菱形继承?
在 C++ 中,菱形继承(Diamond Inheritance)是指一个类(称为派生类)继承两个或多个基类,而这些基类又有一个共同的基类。这种继承结构在类图上呈现出菱形,因此得名。菱形继承在 C++ 中特别常见,因为它允许多重继承,这是 C++ 与其他面向对象语言(如 Java 或 C#)的一个重要区别。
1.1.菱形继承的示例:
class Base {
public:void show() { cout << "Base show" << endl; }
};class Derived1 : public Base {
public:void show() { cout << "Derived1 show" << endl; }
};class Derived2 : public Base {
public:void show() { cout << "Derived2 show" << endl; }
};class Diamond : public Derived1, public Derived2 {
public:void show() { cout << "Diamond show" << endl; }
};
在这个例子中,Diamond
类继承自 Derived1
和 Derived2
,而这两个类又都继承自 Base
类。这就形成了一个菱形继承结构。
1.2.菱形继承的问题:
菱形继承的主要问题是多重继承可能导致的二义性和资源浪费。在上述例子中,Diamond
类会从 Derived1
和 Derived2
继承两个 Base
类的实例,这可能导致以下问题:
-
二义性:如果
Diamond
类需要访问Base
类的成员,编译器可能会不确定应该使用哪个Base
类的实例。 -
资源浪费:每个
Derived1
和Derived2
实例都有自己的Base
实例,这可能导致不必要的内存使用。
#include <iostream>
#include <vector>
#include <algorithm>int main() {std::vector<int> nums = {1, 2, 3, 4, 5};// 使用 lambda 表达式对向量进行排序std::sort(nums.begin(), nums.end(), [](int a, int b) {return a > b; // 降序排序});// 使用 lambda 表达式打印向量中的元素std::for_each(nums.begin(), nums.end(), [](int n) {std::cout << n << " ";});return 0;
}
1.3.解决菱形继承问题:
C++ 提供了几种方法来解决菱形继承的问题:
-
虚继承(Virtual Inheritance):通过使用虚继承,可以确保
Base
类只有一个实例,即使它被多个基类继承。这可以通过在继承列表中使用virtual
关键字来实现。使用虚继承后,Diamond
类将只有一个Base
类的实例。class Base { public:void show() { cout << "Base show" << endl; } };class Derived1 : virtual public Base { // ... };class Derived2 : virtual public Base { // ... };class Diamond : public Derived1, public Derived2 { // ... };
-
接口继承:在某些情况下,可以通过将共同的基类定义为接口(纯虚函数类)来避免菱形继承的问题。
-
重新设计类结构:有时,重新设计类的结构可以避免菱形继承,例如通过使用组合而不是继承。
二、C++中的多线程同步机制?
在 C++ 中,多线程同步是确保多个线程在访问共享资源时能够正确、高效地协调工作的一种机制。C++11 标准引入了多线程支持,提供了一系列的同步工具,包括互斥锁(mutexes)、条件变量(condition variables)、原子操作(atomic operations)等。以下是一些常用的多线程同步机制:
2.1.互斥锁(Mutex):
- 互斥锁用于保护临界区,确保同一时间只有一个线程可以访问共享资源。在 C++ 中,可以使用
std::mutex
来实现互斥锁。
#include <mutex>
#include <thread>std::mutex mtx;
int shared_data = 0;void increment() {mtx.lock();shared_data++;mtx.unlock();
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();return 0;
}
2.2.递归互斥锁(Recursive Mutex):
- 递归互斥锁允许同一个线程多次获得同一互斥锁。在 C++ 中,可以使用
std::recursive_mutex
。
class Singleton {
private:Singleton() {} // 构造函数Singleton(const Singleton&) = delete; // 禁止拷贝Singleton& operator=(const Singleton&) = delete; // 禁止赋值public:static Singleton& getInstance() {static Singleton instance; // 局部静态变量return instance;}
};
2.3.读写锁(Read-Write Lock):
- 读写锁允许多个读线程同时访问共享资源,但写线程在访问时会独占资源。C++ 标准库中没有直接提供读写锁,但可以使用
std::shared_mutex
(C++17 引入)。
2.4.条件变量(Condition Variable):
- 条件变量用于线程间的同步,允许一个或多个线程在某个条件成立之前挂起等待。条件变量通常与互斥锁一起使用。
#include <mutex>
#include <condition_variable>
#include <thread>std::mutex mtx;
std::condition_variable cv;
bool ready = false;void print_id(int id) {std::unique_lock<std::mutex> lock(mtx);while (!ready) {cv.wait(lock);}// 临界区std::cout << "Thread " << id << '\n';
}void go() {std::unique_lock<std::mutex> lock(mtx);ready = true;cv.notify_all();
}int main() {std::thread threads[10];for (int i = 0; i < 10; ++i)threads[i] = std::thread(print_id, i);std::cout << "10 threads ready to race...\n";go();for (auto& th : threads) th.join();return 0;
}
2.5.原子操作(Atomic Operations):
- 原子操作是不可分割的操作,用于在没有互斥锁的情况下同步线程。C++11 引入了
std::atomic
类模板,提供了一种机制来保证操作的原子性。
#include <atomic>
#include <thread>std::atomic<int> ai(0);void increment() {ai++;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();return 0;
}
2.6.屏障(Barrier):
- 屏障是一种同步机制,它允许一组线程相互等待,直到所有线程都到达屏障点。
2.7.信号量(Semaphore):
- 信号量是一种计数器,用于控制对共享资源的访问数量。在 C++ 标准库中没有直接提供信号量,但可以使用第三方库或者自定义实现。
2.8.纤程(Fiber):
- 纤程是一种轻量级的线程,通常用于协程的实现。在 C++20 中,引入了协程的支持,可以通过纤程来实现非阻塞的同步。
-
重新设计类结构:有时,重新设计类的结构可以避免菱形继承,例如通过使用组合而不是继承。
三、如何在c++中创建和管理线程?
在 C++ 中创建和管理线程涉及到几个关键步骤,包括创建线程、执行任务、同步线程以及管理线程生命周期。以下是详细的步骤和示例代码:
3.1.包含必要的头文件:
- 互斥锁用于保护临界区,确保同一时间只有一个线程可以访问共享资源。在 C++ 中,可以使用
std::mutex
来实现互斥锁。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>
3.2.创建线程:
- 使用
std::thread
类来创建线程。你可以将函数或者 lambda 表达式传递给std::thread
的构造函数。
void print_id(int id) {std::cout << "Thread " << id << std::endl;
}int main() {std::thread t1(print_id, 1);std::thread t2(print_id, 2);
}
3.3.等待线程结束:
- 使用
join()
方法等待线程结束。这会阻塞主线程直到指定的线程完成其任务。
t1.join();
t2.join();
3.4.分离线程:
- 使用
detach()
方法可以让线程在后台运行,主线程可以继续执行而不需要等待它结束。
t1.detach();
t2.detach();
3.5.使用线程局部存储:
- 使用
thread_local
存储来定义线程特有的数据,每个线程都有自己的独立副本。
thread_local int thread_local_data = 0;void increment() {thread_local_data++;std::cout << "Thread local data: " << thread_local_data << std::endl;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();
}
3.6.线程同步:
- 使用互斥锁(
std::mutex
)、条件变量(std::condition_variable
)、原子操作(std::atomic
)等同步机制来管理线程间的协作和资源共享。
std::mutex mtx;
std::condition_variable cv;
bool ready = false;void print_message() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, []{ return ready; });std::cout << "Thread is ready to run" << std::endl;
}int main() {std::thread t1(print_message);{std::lock_guard<std::mutex> lock(mtx);ready = true;}cv.notify_one();t1.join();
}
3.7.线程同步:
- 对于需要管理大量线程的场景,可以使用线程池来减少线程创建和销毁的开销。这里是一个简单的线程池示例:
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <iostream>class ThreadPool {
public:ThreadPool(size_t threads) : stop(false) {for(size_t i = 0; i < threads; ++i) {workers.emplace_back([this] {while(true) {std::function<void()> task;{std::unique_lock<std::mutex> 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<class F, class... Args>auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();{std::unique_lock<std::mutex> lock(queue_mutex);if(stop)throw std::runtime_error("enqueue on stopped ThreadPool");tasks.emplace([task](){ (*task)(); });}condition.notify_one();return res;}~ThreadPool() {{std::unique_lock<std::mutex> lock(queue_mutex);stop = true;}condition.notify_all();for(std::thread &worker: workers)worker.join();}private:std::vector<std::thread> workers;std::queue<std::function<void()>> tasks;std::mutex queue_mutex;std::condition_variable condition;bool stop;
};int main() {ThreadPool pool(4);auto result = pool.enqueue([](int answer) { return answer; }, 42);std::cout << "The answer is " << result.get() << std::endl;return 0;
}
3.8.线程同步:
- 在创建线程时,可能会发生错误(例如,系统资源不足)。可以通过检查
std::thread
对象的状态来处理这些错误。
std::thread myThread(task);
if (!myThread.joinable()) {// 处理错误
}
3.9.使用 C++20 协程:
- C++20 引入了协程,它提供了一种更轻量级的线程管理方式,允许在单个线程内以非阻塞的方式执行多个任务。
#include <coroutine>
#include <thread>
#include <iostream>generator<int> GetNumbers() {for (int i = 0; i < 5; ++i) {co_yield i;}
}task<void> RunGenerator() {for (auto n : GetNumbers()) {std::cout << n << std::endl;}
}int main() {std::jthread jthread(RunGenerator());jthread.join();return 0;
}
总结
-
菱形继承:在 C++ 中,菱形继承是一个常见的多重继承问题,它可能导致二义性和资源浪费。通过使用虚继承(virtual inheritance),可以确保基类只有一个共享实例,从而解决这个问题。
-
多线程同步机制:C++ 提供了多种同步机制,包括互斥锁(mutexes)、条件变量(condition variables)、原子操作(atomic operations)等,以确保在多线程环境中对共享资源的安全访问。正确使用这些同步工具对于避免数据竞争和死锁至关重要。
-
创建和管理线程:在 C++ 中,可以通过
std::thread
创建线程,并通过join()
或detach()
管理线程的生命周期。线程同步、线程局部存储和线程池等技术有助于高效地管理线程资源,提高程序的性能和响应能力。
通过深入理解这些概念和机制,开发者可以设计出更高效、更稳定且易于维护的多线程应用程序。随着 C++ 语言的不断发展,新的功能和改进也在不断地被引入,以支持更先进的并发编程模式。