TinyWebSever项目面试题整理
1.为什么要做这样一个项目?
-
满足高并发和高性能需求:现代Web应用面对大量用户,Web服务器需要高效处理并发连接。比如通过线程池、非阻塞I/O、事件驱动机制(如epoll),Web服务器可以有效管理成千上万的并发请求,确保服务不会因高流量而崩溃或变慢。
-
理解网络编程:通过使用线程池、非阻塞socket、epoll等技术,项目可以帮助熟悉Linux下的网络编程模型,深入理解如何处理并发连接、如何进行事件驱动的网络通信等核心技术。
-
实践HTTP协议和Web服务器:构建一个能够解析HTTP请求并进行响应的Web服务器,有助于理解HTTP协议的工作原理,学会如何处理GET和POST请求,增强对Web服务端架构的理解。
-
提升系统优化意识:通过进行性能测试和优化(例如使用Webbench测试并发性能),这个项目还帮助你了解系统性能瓶颈、提升程序的效率、理解并实现高效的并发模型。
2.手写一下线程池
#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include <atomic>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;// 打包任务,保存到shared_ptr中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; // 条件变量,用于任务调度std::atomic<bool> stop; // 停止标志
};int main() {// 创建一个线程池,包含4个工作线程ThreadPool pool(4);// 向线程池添加一些任务并获取结果auto result1 = pool.enqueue([] { return "Hello, "; });auto result2 = pool.enqueue([](const std::string& name) { return name + "World!"; }, "C++ ");// 输出任务结果std::cout << result1.get() << result2.get() << std::endl;return 0;
}
3. 线程的同步机制有哪些?
线程同步机制用于在多线程环境中协调线程的执行,避免数据竞争和资源冲突。常见的线程同步机制有以下几种:
1. 互斥锁(Mutex)
场景: 当多个线程需要安全地修改共享数据时,比如线程要同时操作一个变量、写日志、或者修改数据结构。
特点: 互斥锁就像一个房间的钥匙,只有拿到钥匙的人能进房间修改里面的东西,别人必须等他出来并把钥匙还回去。这种方式确保只有一个线程在修改资源,其他线程必须排队。
适合场景: 适用于简单的“谁进了房间谁就不能让别人进”的场景,比如银行柜台,只有一个人能处理事情,其他人要排队。
2. 读写锁(Read-Write Lock)
场景: 当多个线程需要读取数据,但只有少数线程需要写数据时,比如你有一个资源,大多数线程只是查看它,只有少数线程需要修改它。
特点: 读写锁像图书馆:多个读者(线程)可以同时看书(读取数据),但是一旦有人需要改书(写数据),其他读者必须等这个人改完再看。这种机制允许并发读,减少锁的开销。
适合场景: 适用于“读多写少”的场景,比如在线图书馆,很多人看书,但只有管理员会更新书。
3. 条件变量(Condition Variable)
场景: 适用于某个线程必须等一个条件满足后才能继续工作,比如一个线程在等另一个线程完成任务。
特点: 条件变量就像开会时等老板发言的场景。线程们在“会议室”里等条件满足(比如任务队列有新任务),一旦条件满足了,其他线程被通知并开始工作。
适合场景: 用于生产者-消费者模型,比如有一个任务队列,消费者线程只有在有新任务时才能干活。
4. 信号量(Semaphore)
场景: 控制资源的访问数量,比如限制某个资源同时只能有固定数量的线程使用(比如数据库连接池)。
特点: 信号量就像停车场的闸机,只允许有限数量的车(线程)进入。停车场满了,其他车只能在外面等。有时可以允许多个线程同时访问,但有上限。
适合场景: 适用于控制资源访问数量的场景,比如一个数据库有5个连接池,你只允许最多5个线程同时连接数据库。
5. 自旋锁(Spinlock)
场景: 当锁的等待时间非常短时,比如线程很快就能拿到锁的情况。
特点: 自旋锁像排队等公交车,如果你知道车马上就到,你可能就会站着等,而不会坐下休息(线程不会睡眠,而是不断检查锁是否可用)。它适用于等待时间非常短的场景,因为忙等很浪费资源。
适合场景: 适合非常短时间的临界区,适合多核处理器上多个线程竞争时,开销低于传统的互斥锁。
6. 屏障(Barrier)
场景: 当你有多个线程需要等到所有线程都完成某个阶段后才能继续执行。
特点: 屏障就像体育比赛中,大家必须等所有人到达终点后,才能宣布比赛结束并继续下一项活动。如果你有10个线程,只有当所有线程都执行到某个位置时,大家才继续下一步。
适合场景: 适用于“分阶段”任务,比如多个线程计算一个大任务的不同部分,每部分完成后要同步结果。
7. 原子操作(Atomic Operation)
场景: 适用于对单个变量的简单操作,比如加减计数器。
特点: 原子操作像你一个人占用某个资源,不需要锁和钥匙。这种操作非常简单和快速,不需要锁定整个临界区。
适合场景: 适用于非常轻量的操作,比如多线程增加一个计数器。
8. Future 和 Promise
场景: 用于在线程间传递结果,一个线程做完某个任务并把结果传递给另一个线程。
特点: Future就像点外卖,你下单后可以拿到一个“订单号”(Future对象),当外卖送达(任务完成)时,你就可以用订单号取餐(获得任务结果)。
适合场景: 适用于异步任务处理,特别是当你希望一个线程在另一个线程执行完后得到结果。
9. 任务队列(Task Queue)
场景: 适用于多个线程需要处理一系列任务的场景,任务被生产者线程创建,消费者线程执行任务。
特点: 任务队列像流水线:生产者不断把任务放到队列里,消费者从队列里拿任务来做。使用条件变量或互斥锁来同步任务的生产和消费。
适合场景: 适用于生产者-消费者模型,比如线程池中,线程从任务队列中取任务执行。
总结:通俗理解各个同步机制
- 互斥锁:就像房间钥匙,防止多个线程同时修改资源。
- 读写锁:像图书馆,多个读者能同时看书,但改书时只能一个人来改。
- 条件变量:像会议室,大家等老板发话才能行动。
- 信号量:像停车场,限制同时进入的车的数量。
- 自旋锁:像排队等车,短时间等锁,不用休息。
- 屏障:像比赛,所有线程都到达一个点后才继续。
- 原子操作:快速操作一个变量,不用上锁。
- Future 和 Promise:像点外卖,等任务完成拿结果。
- 任务队列:像流水线,生产者放任务,消费者取任务做。
每种机制都有特定的使用场景和特点,合理选择可以避免资源争用和死锁,确保程序高效运行。
4. 线程池中的工作线程是一直等待吗?
在run函数中,我们为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上,因此项目中线程池中的工作线程是处于 一直阻塞等待 的模式下的。
5. 你的线程池工作线程处理完一个任务后的状态是什么?
(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态
(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。
6. 如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?
该项目采用了I/O多路复用技术,当客户连接有事件需要处理时,epoll会进行事件提醒,然后将对应的任务加入请求队列,等待工作线程的竞争,不仅如此 ,epoll的et(水平触发模式)可以及时的相应处理每一个线程。
7. 如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?
会,因为线程池内线程的数量时有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。
应对策略(定时器):
我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。