每日面经(十)

目录

1.MySQL为什么要定义数据类型

2.epoll的两种模式

3.HTTP请求的格式

4.header表示内容类型的字段

5.get一定没有body吗

 6.为什么需要缓冲区,怎么读数据

7.小根堆定时器

8.阻塞队列是什么,怎么实现阻塞


1.MySQL为什么要定义数据类型

MySQL要定义数据类型主要是为了规范化数据的存储和操作。定义数据类型可以有效地节省存储空间,提高数据的查询和操作效率,并保证数据的完整性和正确性。具体来说,定义数据类型有以下几个优点:

  1. 空间效率:不同的数据类型占用不同的存储空间,在定义表的时候选择合适的数据类型可以有效地节省存储空间。例如,一个整数类型的字段要远比一个字符类型的字段占用更少的存储空间。

  2. 查询效率:对于同一种数据类型的值,相同的比较和操作都可以使用相同的指令,这样可以提高数据的查询和操作效率。例如,在使用WHERE子句进行数据查询时,可以通过指定数据类型的条件来避免隐式数据类型转换,从而提高查询效率。

  3. 数据的完整性和正确性:定义数据类型可以帮助系统检查输入的数据是否符合规定的范围和类型。例如,如果某个字段被定义为整数类型,那么输入非整数类型的数据就会被自动转换或者被拒绝,从而保证数据的完整性和正确性。

  4. 数据的增强安全性:定义数据类型也有助于提高数据的安全性,因为它可以防止恶意操作或者在数据存储时发生非预期的数据类型转换。

综上,它是为了提高数据库的存储效率、查询效率和数据的完整性、正确性,同时也有一定的安全性考虑。

2.epoll的两种模式

epoll实际上有两种工作模式:LT模式和ET模式。

  1. LT模式(水平触发模式):

在LT模式下,epoll_wait()会一直返回就绪的文件描述符,直到该文件描述符变为未就绪状态。当一个文件描述符就绪时,epoll_wait()会通知应用程序进行读或写操作,应用程序可以不立即进行读或写操作,而是可以先进行其他操作(如处理信号等),之后再进行读或写操作。

在LT模式下,如果应用程序未重新读取整个缓冲区中的数据,那么每次epoll_wait()返回时仍会通知应用程序读取该描述符。

  1. ET模式(边缘触发模式):

在ET模式下,当epoll_wait()返回时,应用程序必须立即进行读取操作,否则系统将不再通知该文件描述符就绪事件。 在ET模式下,如果应用程序处理完整个缓冲区中的数据,但未对其进行读取,则每次epoll_wait()返回时不再通知应用程序该文件描述符就绪。

ET模式可以提高程序的效率,但需要程序员保证在epoll_wait()返回时立即进行读取操作。

3.HTTP请求的格式

HTTP 请求报文通常由以下三部分构成:

  1. 请求行:请求方法、请求 URL 和 HTTP 版本组成,例如:GET /index.html HTTP/1.1

  2. 首部字段:包括通用首部、请求首部、实体首部。通用首部包含请求和响应都会使用的首部信息;请求首部包含请求报文特有的信息;实体首部包含描述实体内容的信息。

  3. 消息体:请求报文中的数据,例如 POST 方法时发送的表单数据等。

例如:

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Content-Length: 46

{
    "firstName": "John",
    "lastName": "Doe",
    "email": "johndoe@example.com"
}

这是一个使用 POST 方法提交 JSON 格式数据的 HTTP 请求报文。其中,请求行表示请求的方式和路径;首部字段包含了一些关于请求的元信息,如所请求的主机、消息体的长度和内容类型;消息体包含了实际要提交的数据。

4.header表示内容类型的字段

在 HTTP 请求和响应报文中,使用 Content-Type 首部字段来指示实体内容的 MIME 类型。MIME (Multipurpose Internet Mail Extensions) 是一种互联网标准,用于表示文档、图像、音频、视频以及其他应用程序内的数据。

Content-Type 首部字段的格式为:Content-Type: type/subtype,type 表示主类型,subtype 表示子类型。例如,Content-Type: text/html 表示响应报文的实体主体是 HTML 文本。

常见的实体主体类型包括:

  • text:普通文本,如 HTML、CSS、JavaScript 等;
  • application:二进制数据,如 PDF、Word 文档、JSON、XML 等;
  • image:图像文件,如 JPEG、PNG、GIF 等;
  • audio:声音文件,如 MP3、WAV 等;
  • video:视频文件,如 MP4、AVI 等。

例如,HTTP 请求报文中的 Content-Type 首部字段指示请求数据为 JSON 格式:

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
Content-Length: 46

{
    "firstName": "John",
    "lastName": "Doe",
    "email": "johndoe@example.com"
}

5.get一定没有body吗

在 HTTP 规范中,GET 请求应该没有消息体(message body)。因为 GET 请求是用来请求资源的,而非提交消息实体。

GET 请求应该仅包含请求行和请求头部。请求行包含请求方法、目标资源的 URL 和 HTTP 版本。请求头部包含了客户端在请求中发送的数据,这些数据可以用于进一步指定服务器如何处理该请求。

虽然 HTTP 规范建议 GET 请求不包含消息实体,但实际上,使用 GET 请求时并不一定不能包含消息实体。尤其是在一些特殊场景下,会使用 GET 请求传递一些数据。

对于 GET 请求而言,如果包含消息实体,通常会有以下一些实现上的限制:

  1. 一些浏览器或网关可能会对 GET 请求包含消息实体的请求进行截断或忽略;

  2. 通常情况下,GET 请求的消息实体长度受到浏览器和服务器的限制,而 POST 请求则没有此限制;

  3. GET 请求包含消息实体时会增加请求的复杂度,但并不会增加请求的安全性,因此一些 Web 应用框架不支持在 GET 请求中包含消息实体。

综上所述,虽然理论上可以在 GET 请求中添加消息实体,但实际情况下,这种做法是不建议的。如果需要在请求中传递数据,可以使用 POST 方法或在 URL 中添加查询参数。

 6.为什么需要缓冲区,怎么读数据

在程序中经常需要读写文件或网络数据,这些操作可能会涉及到大量的数据。对于这些数据的读取和处理,为了提高效率,通常会采用缓冲区的方式来进行。

缓冲区是指程序将一段连续的内存空间作为临时存储区域,用来暂时存储数据,等到数据处理完成后再统一写入文件或网络中。缓冲区可以有效减少频繁读写 IO 操作对系统带来的开销,提高 CPU 和 I/O 操作之间的效率。

对于数据的读取,通常会使用一些 I/O 系统调用来完成,例如 Linux 下的 read()、recv() 等函数。这些函数通常会将数据读入到指定的缓冲区中,并返回实际读取的字节数。在读取数据时,需要根据实际读取的字节数来进行处理,可以使用循环来多次读取数据,直到读取到预期的数据量为止。

下面是一个使用缓冲区进行文件读取的示例代码:

#include <cstdio>

int main() {
    char buf[1024];
    FILE* fp = fopen("file.txt", "r");
    if (fp == nullptr) {
        // 文件打开失败
        return -1;
    }
    // 读取文件内容
    while (!feof(fp)) {
        int n = fread(buf, 1, sizeof(buf), fp);
        // 处理读取的内容
        printf("%.*s", n, buf);
    }
    // 关闭文件
    fclose(fp);
    return 0;
}

在上述代码中,使用一个大小为 1024 字节的数组作为缓冲区,循环读取文件内容,每次读取的数据存储到缓冲区中。然后使用 printf() 函数输出读取的内容,其中 %.*s 表示输出字符串的长度由第一个参数指定,第二个参数为字符串指针。当文件读取到末尾时,feof() 函数会返回非零值,循环退出。最后,使用 fclose() 函数关闭文件。

7.小根堆定时器

小根堆定时器是一种常见的实现定时器功能的方式,它通常用于在大规模的并发应用中,轻量级地管理大量的定时任务。下面简单介绍一下小根堆定时器的基本思想和实现过程。

  1. 基本思想

小根堆定时器通常维护一个小根堆,堆中的每个节点表示一个定时任务。每个节点中包含任务的运行时间和任务的回调函数等信息。堆中的根节点是最小的节点,表示堆中最近要到期的任务。

当一个新的定时任务到来时,将其插入到小根堆中。当堆中的任务定时时间到达时,堆会弹出根节点,并执行该任务的回调函数。

  1. 实现步骤

下面是一个简单的实现步骤:

  1. 定义一个小根堆的数据结构,用于维护任务的时间顺序。堆中的每个节点应包含任务的运行时间、任务的回调函数等信息。

  2. 当一个新的定时任务到来时,将其插入到小根堆中。

  3. 每隔一段时间(例如 1 毫秒),从小根堆中取出所有已经到期的任务,并执行其回调函数。然后将已经到期的任务从堆中移除。

  4. 重复步骤 3,直到堆为空。

下面是一个简单的 C++ 代码实现:

#include <chrono>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>

class Timer {
 private:
  using Clock = std::chrono::high_resolution_clock;
  using TimePoint = std::chrono::time_point<Clock>;
  using Task = std::pair<TimePoint, std::function<void()>>;

  std::mutex mutex_;
  std::priority_queue<Task, std::vector<Task>, std::greater<Task>> heap_;
  std::thread thread_;
  std::condition_variable cv_;
  bool stop_ = false;

 public:
  Timer() {
    thread_ = std::thread([this]() {
      while (!stop_) {
        std::unique_lock<std::mutex> lock(mutex_);
        if (heap_.empty()) {
          cv_.wait(lock);
        } else {
          const auto& task = heap_.top();
          const auto& now = Clock::now();
          if (now < task.first) {
            cv_.wait_until(lock, task.first);
          } else {
            heap_.pop();
            lock.unlock();
            task.second();
          }
        }
      }
    });
  }

  ~Timer() {
    stop_ = true;
    cv_.notify_all();
    thread_.join();
  }

  void addTask(TimePoint tp, std::function<void()> f) {
    std::unique_lock<std::mutex> lock(mutex_);
    heap_.emplace(tp, f);
    cv_.notify_all();
  }
};

在上述代码中,我们定义了一个 Timer 类,它维护了一个小根堆 heap_,用于保存所有待执行的任务信息。当一个新的任务到来时,将其插入到 heap_ 中。

Timer 类还包含了一个线程 thread_,用于定期检查任务是否到期。每次 thread_ 检查时,它会从 heap_ 取出最小的任务,如果该任务的时间还没到,就会进入等待状态;否则,会执行该任务的回调函数,并从 heap_ 中移除该任务。每次执行完一批任务后,thread_ 会检查 heap_ 是否为空,如果为空,就进入等待状态。

当 Timer 对象被销毁时,我们将 stop_ 标志设置为 true,然后通知线程 thread_ 终止运行。

Timer 类的使用方式如下:

Timer timer;
auto tp1 = std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
auto tp2 = std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
auto tp3 = std::chrono::steady_clock::now() + std::chrono::milliseconds(300);
timer.addTask(tp1, []() { std::cout << "Task 1\n"; });
timer.addTask(tp2, []() { std::cout << "Task 2\n"; });
timer.addTask(tp3, []() { std::cout << "Task 3\n"; });

在上述代码中,我们创建了一个 Timer 对象,然后添加了三个定时任务。这些定时任务分别在 100 毫秒、200 毫秒和 300 毫秒后执行。执行定时任务时,会输出相应的文本信息。

8.阻塞队列是什么,怎么实现阻塞

阻塞队列是一种线程安全的队列,它支持在队列为空或队列已满时进行阻塞等待。当队列为空时,尝试获取元素的线程会被阻塞,直到队列中有新的元素被加入;当队列已满时,尝试往队列中添加元素的线程会被阻塞,直到队列中有元素被取出。

阻塞队列的实现分为两种基本方式:使用条件变量实现阻塞、使用信号量实现阻塞。

  1. 使用条件变量实现阻塞

条件变量是 pthread 库提供的一种基本同步机制,它通常和互斥量一起使用,用于线程之间的互斥和通信。在实现阻塞队列时,可以使用两个条件变量表示队列是否为空和是否已满,以及一个互斥锁用于保护队列的数据结构。具体实现可以参考下面的代码:

#include <mutex>
#include <queue>
#include <condition_variable>

template <typename T>
class BlockingQueue {
 public:
  BlockingQueue(int maxSize) : maxSize_(maxSize) {}

  void put(const T& x) {
    {
      std::unique_lock<std::mutex> lock(mutex_);
      while (queue_.size() == maxSize_) {
        notFull_.wait(lock);
      }
      queue_.push(x);
    }
    notEmpty_.notify_one();
  }

  T take() {
    T x;
    {
      std::unique_lock<std::mutex> lock(mutex_);
      while (queue_.empty()) {
        notEmpty_.wait(lock);
      }
      x = queue_.front();
      queue_.pop();
    }
    notFull_.notify_one();
    return x;
  }

 private:
  std::queue<T> queue_;
  std::mutex mutex_;
  std::condition_variable notEmpty_;
  std::condition_variable notFull_;
  int maxSize_;
};

在上述代码中,我们定义了一个 BlockingQueue 模板类,可以支持任意类型的队列元素。队列中最多可以存储 maxSize_ 个元素。

put() 函数用于往队列中添加元素。一开始使用互斥锁(mutex_)进行保护,当队列已满时,使用 notFull_ 条件变量进行等待,直到队列不满为止。然后将元素加入到队列中,并使用 notEmpty_ 条件变量通知可能正在等待的线程有新元素加入了队列。

take() 函数用于从队列中取出元素。一开始使用互斥锁进行保护,当队列为空时,使用 notEmpty_ 条件变量进行等待,直到队列不为空为止。然后取出队列中的第一个元素,并使用 notFull_ 条件变量通知可能正在等待的线程有队列中的空间空出来了。

  1. 使用信号量实现阻塞

信号量是一种基本同步机制,它包含一个计数器和两个原子操作:wait() 和 signal()。wait() 用于尝试获取资源,如果计数器大于 0,就减去一个资源并返回;否则,wait() 就会被阻塞。signal() 用于释放资源,将计数器加一,如果有线程正在等待该资源,就将其中一个唤醒。

在实现阻塞队列时,可以使用两个信号量表示队列是否为空和是否已满,以及一个互斥锁用于保护队列的数据结构。具体实现可以参考下面的代码:

#include <mutex>
#include <queue>
#include <semaphore.h>

template <typename T>
class BlockingQueue {
 public:
  BlockingQueue(int maxSize) : maxSize_(maxSize) {
    sem_init(&notEmpty_, 0, 0);
    sem_init(&notFull_, 0, maxSize);
  }

  void put(const T& x) {
    sem_wait(&notFull_);
    {
      std::lock_guard<std::mutex> lock(mutex_);
      queue_.push(x);
    }
    sem_post(&notEmpty_);
  }

  T take() {
    sem_wait(&notEmpty_);
    T x;
    {
      std::lock_guard<std::mutex> lock(mutex_);
      x = queue_.front();
      queue_.pop();
    }
    sem_post(&notFull_);
    return x;
  }

 private:
  std::queue<T> queue_;
  std::mutex mutex_;
  sem_t notEmpty_;
  sem_t notFull_;
  int maxSize_;
};

在上述代码中,我们定义了一个 BlockingQueue 模板类,可以支持任意类型的队列元素。队列中最多可以存储 maxSize_ 个元素。

put() 函数用于往队列中添加元素。一开始使用信号量 notFull_ 进行等待,直到队列不满为止。然后使用互斥锁保护队列,将元素加入到队列中,并使用信号量 notEmpty_ 通知可能正在等待的线程有新元素加入了队列。

take() 函数用于从队列中取出元素。一开始使用信号量 notEmpty_ 进行等待,直到队列不为空为止。然后使用互斥锁保护队列,取出队列中的第一个元素,并使用信号量 notFull_ 通知可能正在等待的线程有队列中的空间空出来了。

总之,阻塞队列能够方便地应用于生产者-消费者模型中,常常被用于协调多个线程之间的任务调度。使用条件变量或信号量可以实现阻塞队列,实现方式相对独立,可以根据实际需求选择不同的实现方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值