C++ TinyWebServer项目总结(12. 高性能I/O框架库Libevent)

news2024/9/20 16:53:51

Linux服务器程序必须处理三类事件(I/O、信号和定时事件),在处理这三类事件时需要考虑以下问题:

  1. 统一事件源。统一处理这三类事件既能使代码简单易懂,又能避免一些潜在的逻辑错误。实现统一事件源的一般方法:利用 I/O复用系统调用来管理所有事件。
  2. 可移植性。不同的操作系统有不同的I/O复用方式,如Solaris的dev/poll文件、FreeBSD的kqueue机制、Linux的epoll系列系统调用。
  3. 对并发编程的支持。在多进程和多线程环境下,我们需要考虑各执行实体如何协同处理客户连接、信号、定时器,以避免竞态条件。

开源社区提供了很多优秀的开源I/O框架库,它们不仅解决了以上问题,让开发者可以将精力完全放在程序的逻辑上,而且稳定性、性能等各方面都相当出色,如ACE、ASIO 和 Libevent,本章介绍其中相对轻量级的Libevent框架库。

I/O 框架库概述

I/O框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。

各种I/O框架库的实现基本原理相似,要么以Reactor模式实现,要么以Proactor模式实现,要么同时以这两种模式实现。例如,基于Reactor模式的I/O框架库包含以下组件:句柄(Handle)、事件多路分发器(EventDemultiplexer)、事件处理器(EventHandler)、具体的事件处理器(ConcreteEventHandler)、Reactor。这些组件的关系见下图:

句柄(Handle)

I/O框架库要处理的对象,即I/O事件、信号、定时事件,统一称为事件源。一个事件源通常和一个句柄绑定在一起。句柄的作用是,当内核检测到就绪事件时,它将通过句柄来通知应用进程这一事件。在Linux环境下,I/O事件对应的句柄是文件描述符,信号事件对应的句柄就是信号值。

事件多路分发器(EventDemultiplexer)

事件的到来时随机的、异步的,我们无法预知进程何时收到一个客户连接请求,或收到一个暂停信号,所以进程需要循环地等待并处理事件,这就是事件循环。在事件循环中,等待时间一般使用I/O复用技术来实现。I/O框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是selectpollepoll_wait等函数。

此外,事件多路分发器还需实现register_eventremove_event方法,以供调用者往事件多路分发器中添加事件和从事件多路分发器中删除事件。

事件处理器(EventHandler)和具体事件处理器(ConcreteEventHandler)

事件处理器执行事件对应的业务逻辑。它通常包含一个或多个handle_event回调函数,这些回调函数在事件循环中被执行。I/O框架库提供的事件处理器通常是一个接口,用户需要继承它来实现自己的事件处理器,即具体事件处理器,因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。

此外,事件处理器一般还提供get_handle方法,它返回与该事件处理器关联的句柄。当事件多路分发器检测到有事件发生时,它是通过句柄来通知应用进程的,由于我们将句柄和事件处理器绑定,才在事件发生时获取到正确的事件处理器。

Reactor

它是I/O框架库的核心,它提供的几个主要方法是:

  1. handle_events。该方法执行事件循环,它重复以下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
  2. register_handler。该方法调用事件多路分发器的register_event方法来往事件多路分发器中注册一个事件。
  3. remove_handler。该方法调用事件多路分发器的remove_event方法来删除事件多路分发器中的一个事件。

I/O 框架库的工作时序:

Libevent 源码分析

Libevent是开源的高性能I/O框架库,使用Livevent的著名案例有:高性能的分布式内存对象缓存软件memcached,Google浏览器Chromiun的Linux版本。Libevent的特点:

  1. 跨平台支持。Libevent支持Linux、UNIX、Windows。
  2. 统一事件源。Libevent对I/O事件、信号、定时事件提供统一的处理。
  3. 线程安全。Libevent使用libevent_pthreads库来提供线程安全支持。
  4. 基于Reactor模式实现。

Libevent的官网是libevent,其中提供Libevent源码的下载,以及Libevent框架库的第一手文档,且源码和文档的更新也较为频繁。作者游双大佬写书时使用的Libevent版本是2.0.19。

event 结构体

Libevent中的事件处理器是event结构类型,event结构体封装了句柄、事件类型、回调函数、其他必要的标志和数据,该结构体在include/event2/event_struct.h文件中定义:

struct event
{
    TAILQ_ENTRY(event) ev_active_next; /* 活动事件队列 */
    TAILQ_ENTRY(event) ev_next; /* 注册事件队列 */
    union {
        TAILQ_ENTRY(event) ev_next_with_common_timeout;
        int min_heap_idx;
    } ev_timeout_pos;
    evutil_socket_t ev_fd;
    struct event_base *ev_base;
    
    union {
        struct {
            TAILQ_ENTRY(event) ev_io_next;
            struct timeval ev_timeout;
        } ev_io;
        
        struct {
            TAILQ_ENTRY(event) ev_signal_next;
            short ev_ncalls;
            short *ev_pncalls;
        } ev_signal;
    } _ev;
    
    short ev_events; /* 事件类型 */
    short ev_res;
    short ev_flags;
    ev_uint8_t ev_pri;
    ev_uint8_t ev_closure;
    struct timeval ev_timeout;
    
    void (*ev_callback)(evutil_socket_t, short, void *arg);
    void *ev_arg;
};

下面介绍event结构体中成员:

  • ev_events。它代表事件类型。
  • ev_next。所有已经注册的事件处理器(包括I/O事件处理器和信号事件处理器)通过该成员串联成一个尾队列,我们称之为注册事件队列。宏TAILQ_ENTRY是尾队列中的节点类型,它定义在compat/sys/queue.h文件中:
#define TAILQ_ENTRY(type)
struct {
    struct type* tqe_next;		/*下一个元素 */
    struct type**tqe_prev;		/*前一个元素的地址*/
}
  • ev_active_next。所有被激活的事件处理器通过该成员串联成一个尾队列,我们称之为活动事件队列。活动事件队列不止一个,不同优先级的事件处理器被激活后将被插入不同的活动事件队列中。在事件循环中,Reactor将按优先级从高到低遍历所有活动事件队列,并依次处理其中的事件处理器。
  • ev_timeout_pos。这是一个联合体,它仅用于定时事件处理器,为讨论方便,后面我们称定时事件处理器为定时器,老版本的Libevent中,定时器都是由时间堆来管理的,但开发者认为有时使用简单的链表来管理定时器效率更高,因此,新版本Libevent引入了通用定时器的概念,这些定时器不是存储在时间堆中,而是存储在尾队列中,我们称之为通用定时器队列,对于通用定时器而言,ev_timeout_pos联合体的ev_next_with_common_timeout成员指出了该定时器在通用定时器队列中的位置,对于其他定时器而言,ev_time_pos联合体的min_heap_idx成员指出了该定时器在时间堆中的位置。一个定时器是否是通用定时器取决于其超时值大小,具体判断原则可参考event.c文件中的is_common_timeout函数。
  • _ev。这是一个联合体,所有具有相同文件描述符值的I/O事件处理器通过ev.ev_io.ev_io_next成员串联成一个尾队列,我们称之为I/O事件队列;所有具有相同信号值的信号事件处理器通过ev.ev_signal.ev_signal_next成员串联成一个尾队列,我们称之为信号事件队列,ev.ev_signal.ev_ncalls成员指定信号事件发生时,Reactor需要执行多少次该事件对应的事件处理器中的回调函数,ev.ev_signal.ev_pncalls指针成员要么是NULL,要么指向ev.ev_signal.ev_ncalls

在程序中,我们可能针对同一个socket文件描述符上的可读(可写)事件创建多个事件处理器(它们拥有不同的回调函数),当该文件描述符上有可读(可写)事件发生时,所有这些事件处理器都应该被处理,所以Libevent使用I/O事件队列将具有相同文件描述符值的事件处理器组织在一起,这样,当一个文件描述符上有事件发生时,事件多路分发器能很快把所有相关的事件处理器添加到活动事件队列中。信号事件队列的存在也是由于相同的原因。

  • ev_fd。对于I/O事件处理器,它是文件描述符值;对于信号事件处理器,它是信号值。
  • ev_base。该事件处理器从属的event_base实例。
  • ev_res。它记录当前激活事件的类型。
  • ev_flags。它是一些事件标志。
  • ev_pri。它指定事件处理器的优先级,值越小优先级越高。
  • ev_closure。它指定event_base执行事件处理器的回调函数时的行为,其可选值定义在event-internal.h文件中:
  • ev_timeout。它仅对定时器有效,指定定时器的超时值。
  • ev_callback。它是事件处理器的回调函数,由event_base调用,回调函数被调用时,它的3个参数分别被传入事件处理器的以下3个成员:ev_fdev_resev_arg
  • ev_arg。回调函数的参数。

往注册事件队列中添加事件处理器

创建一个event对象的函数是event_new(及其变体),它在event.c文件中实现,该函数很简单,主要给event对象分配内存并初始化它的部分成员,因此我们不讨论它。event对象创建好后,应用需要调用event_add函数将其添加到注册事件队列中,并将对应的事件注册到事件多路分发器上。event_add函数在event.c文件中实现,主要是调用另一个内部函数event_add_internal,它的内部调用了几个重要函数:

  • evmap_io_add。该函数将I/O事件添加到事件多路分发器中,并将对应事件处理器添加到I/O事件队列中,同时建立I/O 事件和I/O事件处理器之间的映射关系。
  • evmap_signal_add。该函数将信号事件添加到事件多路分发器中,并将对应的事件处理器添加到信号事件队列中,同时建立信号事件和信号事件处理器之间的映射关系。
  • event_queue_insert。该函数将事件处理器添加到各种事件队列中:将I/O事件处理器和信号事件处理器插入注册事件队列;将定时器插入通用定时器队列或时间堆;将被激活的事件处理器添加到活动事件队列中。

往事件多路分发器中注册事件

以上event_queue_insert函数所做的仅仅是将一个事件处理器加入event_base的某个事件队列中,对于新添加的I/O事件处理器和信号事件处理器,我们还需让事件多路分发器来监听其对应的事件,同时建立文件描述符、信号值、事件处理器之间的映射关系,这需要通过evmap_io_addevmap_signal_add函数来完成,这两个函数相当于事件多路分发器中的register_event方法,它们由evmap.c文件实现。

eventop 结构体

eventop结构体封装了I/O复用机制必要的一些操作,如注册事件、等待事件。它为event_base支持的所有后端I/O复用机制提供了一个统一的接口,该结构体定义在event-internal.h文件中:

struct eventop {
    // 后端I/O复用技术的名称
    const char *name;
    // 初始化函数
    void *(*init)(struct event_base *);
    // 注册事件
    int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
    // 删除事件
    int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
    // 等待事件
    int (*dispatch)(struct event_base *, struct timeval *);
    // 释放I/O复用机制使用的资源
    void (*dealloc)(struct event_base *);
    // 程序调用fork后是否需要重新初始化event_base
    int need_reinit;
    // I/O复用技术支持的一些特性,是以下可选值的按位或:
    // EV_FEATURE_ET:支持边沿触发事件EV_ET
    // EV_FEATURE_O1:事件检测算法的复杂度是O(1)
    // EV_FEATURE_FDS:不仅能监听socket上的事件,还能监听其他类型文件描述符上的事件
    enum event_method_feature features;
    // 有的I/O复用机制需要为每个I/O事件队列和信号事件队列分配额外的内存,该内存用于存放文件描述符
    // 以避免同一个文件描述符被重复插入IO复用机制的事件表中
    // evmap_io_add和evmap_io_del函数在调用eventop的add或del方法时,将这段内存的起始地址传递给该方法
    // fdinfo_len指定了这段内存的长度
    size_t fdinfo_len;
};

devpoll.ckqueue.cevport.cselect.cwin32select.cpoll.cepoll.c文件分别针对不同的I/O复用技术实现了eventop定义的这套接口,在支持多种I/O复用技术的系统上,Libevent选择使用哪个取决于这些I/O复用技术的优先级。Libevent支持的后端I/O复用技术及它们的优先级定义在event.c文件中,在Linux下,Libevent默认选择的后端I/O复用技术是epoll

event_base 结构体

结构体event_base是Libevent的Reactor,它定义在event-internal.h文件中:

struct event_base {
    // 初始化Reactor时选择一种后端I/O复用机制,并记录在该字段中
    const struct eventop *evsel;
    // 指向I/O复用机制真正存储的数据,它通过evsel成员的init函数来初始化
    void *evbase;
    // 事件变化队列,用途是:如果一个文件描述符上注册的事件被多次修改,则可使用缓冲来避免重复的系统调用(如epoll_ctl函数)
    // 它仅能用于时间复杂度为O(1)的IO复用技术
    struct event_changelist changelist;
    // 指向信号的后端处理机制,目前仅在signal.h文件中定义了一种处理方法
    const struct eventop *evsigsel;
    // 信号事件处理器使用的数据结构,其中封装了一个由socketpair函数创建的管道
    // 它用于信号处理函数和事件多路分发器之间的通信,与统一事件源的思路相同
    struct evsig_info sig;
    // 添加到该event_base的虚拟事件、所有事件、激活事件的数量
    int virtual_event_count;
    int event_count;
    int event_count_active;
    // 是否执行完活动事件队列上剩余的任务后就退出事件循环
    int event_gotterm;
    // 是否立即退出事件循环,而不管是否还有任务需要处理
    int event_break;
    // 是否应启动一个新的事件循环
    int event_continue;
    // 目前正在处理的活动事件队列的优先级
    int event_running_priority;
    // 事件循环是否已启动
    int running_loop;
    // 活动事件队列数组,索引值越小的队列,优先级越高,高优先级的活动事件队列中的事件处理器将被优先处理
    struct event_list *activequeues;
    // 活动事件队列数组的大小,即该event_base一共有nactivequeues个不同优先级的活动事件队列
    int nactivequeues;
    // 以下3个成员管理通用定时器队列
    struct common_timeout_list **common_timeout_queues;
    int n_common_timeouts;
    int n_common_timeouts_allocated;
    // 存放延迟回调函数的链表,事件循环每次成功处理完一个活动事件队列中的所有事件后,就调用一次延迟回调函数
    struct deferred_cb_queue defer_queue;
    // 文件描述符和I/O事件之间的映射关系表
    struct event_io_map io;
    // 信号值和信号事件之间的映射关系表
    struct event_signal_map sigmap;
    // 注册事件队列,存放I/O事件处理器和信号事件处理器
    struct event_list eventqueue;
    // 时间堆
    struct min_heap timeheap;
    // 管理系统时间的一些成员
    struct timeval event_tv;
    struct timeval tv_cache;
#if defined(_EVENT_HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
    struct timeval tv_clock_diff;
    time_t last_updated_clock_diff;
#endif

// 多线程支持
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
    // 当前运行该event_base的事件循环的线程
    unsigned long th_owner_id;
    // 对event_base的独占锁
    void *th_base_lock;
    // 当前事件循环正在执行哪个事件处理器的回调函数
    struct event *current_event;
    // 条件变量,用于唤醒正在等待某个事件处理完毕的线程
    void *current_event_cond;
    // 等待current_event_cond的线程数
    int current_event_waiters;
#endif

#ifdef WIN32
    struct event_iocp_port *iocp;
#endif

    // 该event_base的一些配置参数
    enum event_base_config_flag flags;
    // 以下成员给工作线程唤醒主线程提供了方法(使用socketpair函数创建的管道)
    int is_notify_pending;
    evutil_socket_t th_notify_fd[2];
    struct event th_notify;
    int (*th_notify_fn)(struct event_base *base);
};

事件循环

最后讨论一下Libevent的动力,即事件循环。Libevent中实现事件循环的函数是event_base_loop,该函数首先调用I/O事件多路分发器的事件监听函数,以等待事件,当有事件发生时,就依次处理之。

实战 9:利用 Libevent 库实现一个“Hello World”程序

代码位于:

#include <sys/signal.h>
#include <event.h>

/* 这个回调函数在捕获到信号后被调用,它设定了一个两秒后的延迟退出事件循环。 */
void signal_cb(int fd, short event, void *argc) {
    struct event_base *base = (event_base *)argc;
    struct timeval delay = {2, 0};
    printf("Caught an interrupt signal; exiting cleanly in two seconds...\n");
    event_base_loopexit(base, &delay);
}

void timeout_cb(int fd, short event, void *argc) {
    printf("timeout\n");
}

int main() {
    struct event_base *base = event_init();

    /* 这里创建了一个信号事件处理器,用来捕获 SIGINT(通常是用户按下 Ctrl+C 生成的中断信号)。当信号发生时,调用 signal_cb 回调函数。 */
    struct event *signal_event = evsignal_new(base, SIGINT, signal_cb, base);
    event_add(signal_event, NULL);

    /* 创建一个定时事件处理器,1秒后触发一次 timeout_cb 回调函数。 */
    timeval tv = {1, 0};
    struct event *timeout_event = evtimer_new(base, timeout_cb, NULL);
    event_add(timeout_event, &tv);

    /* 开始事件处理循环,这行代码将会阻塞,直到事件循环被终止。 */
    event_base_dispatch(base);

    /* 资源释放 */
    event_free(timeout_event);
    event_free(signal_event);
    event_base_free(base);
}

首先确保安装了 Libevent 库。 编译程序时,需要链接 libevent 库:

gcc -o example example.cpp -levent

运行程序,然后按 Ctrl+C 触发信号处理, 查看程序如何响应并在两秒后优雅退出。

程序启动后,经过 1s 触发定时事件处理器的回调函数,执行printf("timeout\n");,按下Ctrl+C 后,

执行信号事件处理器的回调函数,printf("Caught an interrupt signal; exiting cleanly in two seconds...\n");并在 2s 后退出。

代码解析

  1. 调用event_init函数创建event_base对象。一个event_base对象相当于一个Reactor实例。event_base 是 libevent 处理事件的核心结构。
struct event_base *base = event_init();
  1. 创建具体的事件处理器,并设置它们所从属的Reactor实例。evsignal_newevtimer_new分别用于创建信号事件处理器和定时事件处理器,它们是定义在include/event2/event.h文件中的宏:
#define evsignal_new(b, x, cb, arg) \
    event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
#define evtimer_new(b, cb, arg) event_new((b), -1, 0, (cb), (arg)) 

它们的统一入口是event_new函数,即用于创建通用事件处理器(图12-1中的EventHandler)的函数,其定义是:

struct event* event_new(struct event_base* base, evutil_socket_t fd, short events, 
                            void (*cb)(evutil_socket_t, short, void*), void* arg);
  • base:指定新创建的事件处理器的从属Reactor。
  • fd:指定与该事件处理器关联的句柄。对于I/O事件处理器,fd是文件描述符;对于信号事件处理器,fd是信号值;对于定时事件处理器,要给fd传递-1。
  • events:指定事件类型

  • cb:指定目标事件对应的回调函数,相当于图12-1中事件处理器的handle_event回调函数。
  • arg:Reactor传递给回调函数的参数。

event_new函数成功时返回一个event类型的对象,即 libevent的事件处理器。Libevent用event描述事件处理器,而不是事件,可能会使读者混乱,因此我们有如下约定:

  • 事件指的是一个句柄上绑定的事件,如文件描述符0上的可读事件。
  • 事件处理器,也就是event结构体对象,除了包含事件必须具备的两个要素(句柄和事件类型)外,还有其他成员,如回调函数。
  • 事件由事件多路分发器管理,事件处理器则由事件队列管理。事件队列包括多种,如event_base中的注册事件队列、活动事件队列、通用定时器队列,以及evmap中的I/O事件队列、信号事件队列。
  • 事件循环对一个被激活事件(就绪事件)的处理,指的是执行该事件对应的事件处理器中的回调函数。
  1. 调用event_add函数,将事件处理器添加到注册事件队列中,并将该事件处理器对应的事件添加到事件多路分发器中。event_add函数相当于Reactor中的register_handler方法。
  2. 调用event_base_dispatch指定事件循环。
  3. 时间循环结束后,使用*_free系列函数释放系统资源。

参考文章

  1. Linux高性能服务器编程 学习笔记 第十二章 高性能IO框架库Libevent-CSDN博客
  2. Linux高性能服务器编程-游双——第十二章 高性能IO框架库Libevent_游双的linux高性能服务器编-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2079478.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何用Java SpringBoot+Vue搭建花开富贵花园管理系统

&#x1f393; 作者&#xff1a;计算机毕设小月哥 | 软件开发专家 &#x1f5a5;️ 简介&#xff1a;8年计算机软件程序开发经验。精通Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等技术栈。 &#x1f6e0;️ 专业服务 &#x1f6e0;️ 需求定制化开发源码提…

推荐一篇 学习SQL 的文章

学习 java&#xff0c;当然避不开数据库的知识&#xff0c;个人认为好学好理解的一篇文章&#xff0c;推荐给大家 SQL语法基础知识总结 | JavaGuide「Java学习 面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试&#xff0c;首选 JavaGuide&#xf…

智能优化算法-鹈鹕优化算法(POA)(附源码)

目录 1.内容介绍 2.部分代码 3.实验结果 4.内容获取 1.内容介绍 鹈鹕优化算法 (Pelican Optimization Algorithm, POA) 是一种基于群体智能的元启发式优化算法&#xff0c;它模拟了鹈鹕的捕食行为和社会交互特性&#xff0c;用于解决复杂的优化问题。 POA的工作机制主要包括…

单元测试、系统测试和集成测试知识详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、单元测试的概念 单元测试是对软件基本组成单元进行的测试&#xff0c;如函数或一个类的方法。当然这里的基本单元不仅仅指的是一个函数或者方法&#xff…

FlagEval 8月榜 | 文生视频大模型主观评测结果揭晓,新增6款新发布模型

近日&#xff0c;智源研究院联合中国传媒大学发布文生视频大模型主观评测榜单&#xff0c;在今年5月对Sora、Runway Gen-2、PixVerse V1、Pika 1.0、VideoCrafter-V2、Show-1、Open-Sora 1.0七个模型性能表现评测结果的基础之上&#xff0c;不仅对部分模型的升级版本进行了对比…

C++初学(16)

16.1、if语句 当C程序必须决定是否执行某个操作时&#xff0c;通常使用if语句来实现选择。if有两种格式&#xff1a;if和if else。 if语句的语法与while相似&#xff1a; if(text-condition)statement 如果text-condition&#xff08;测试条件&#xff09;为true&#xff0…

iTimes工时管理系统:企业高效管理的得力助手

在当今快节奏的商业环境中&#xff0c;企业面临着越来越多的挑战&#xff0c;其中之一便是如何高效、准确地管理员工工时。工时管理不仅关乎企业的成本控制&#xff0c;还直接影响到项目执行效率、员工满意度以及整体运营水平。因此&#xff0c;选择一款优秀的工时管理系统显得…

【应用层】Tomcat10安装以及对应的VScode插件使用

文章日期是2024年8月26日&#xff0c;Tomcat10为稳定版中最新的&#xff0c;Tomcat11为测试版。 流程&#xff1a;下载Tomcat10-->等待下载时&#xff0c;安装对应的VScode插件-->配置Tomcat10-->配置对应的VScode插件 1、下载Tomcat10 2、安装对应的VScode插件 3…

Codeforce 963

CF 963 B 模拟加贪心 偶数个数C 模拟前缀和 灯能否全亮D 二分DP 中位数尽可能大F1 模拟镜像 题目链接 B 模拟加贪心 偶数个数 考点&#xff1a;贪心 思路&#xff1a;除了全是偶数的情况&#xff0c;其他的情况都需要将偶数转换为奇数。最少的操作步数是偶数个数&#xff0c;…

IOS 15 实现Toast和小菊花Loading提示

本文主要是实现toast和loading两种提示功能&#xff0c;例如&#xff1a;登陆时参数不正确提示&#xff0c;toast提示后会自动隐藏。加载提示&#xff1a;不会自动隐藏&#xff0c;常用于网络请求&#xff0c;上传等。 添加依赖 #提示框架 #https://github.com/jdg/MBProgress…

20240828 每日AI必读资讯

8岁女孩玩转AI编程&#xff0c;45分钟打造聊天机器人&#xff0c;Karpathy都看呆了 - 新晋顶流AI代码编辑器——Cursor&#xff0c;已经进化到了“0手工代码”阶段。 - 提供了多个AI模型&#xff0c;包括GPT-4、GPT-4o和Claude 3.5 Sonnet等&#xff0c;可以通过跟大模型聊天…

一文弄懂MySQL中的锁

MySQL中的锁概述 MySQL中的锁机制是数据库管理系统用于控制并发操作的一种手段&#xff0c;主要用于保证数据的一致性和完整性。当多个事务同时操作同一数据时&#xff0c;锁机制可以防止数据冲突和确保事务的隔离性。 在MySQL中&#xff0c;锁可以分为三大类&#xff1a;全局…

如何用Python Django和Vue构建网络电视剧收视率分析系统?

&#x1f393; 作者&#xff1a;计算机毕设小月哥 | 软件开发专家 &#x1f5a5;️ 简介&#xff1a;8年计算机软件程序开发经验。精通Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等技术栈。 &#x1f6e0;️ 专业服务 &#x1f6e0;️ 需求定制化开发源码提…

《计算机操作系统》(第4版)第11章 多媒体操作系统 复习笔记

第11章 多媒体操作系统 一 、多媒体系统简介 1. 多媒体的概念 多媒体 (multimedia) 目前没有统一的定义&#xff0c;一般是指多种方法、多种形态传输(传播)的信息介质、多种 载体的表现形式以及多种存储、显示和传递方式。 2.超文本和超媒体 (1)超文本 (hypertext)。 (2)超链接…

探索Python性能监控的瑞士军刀:psutil的神秘面纱

文章目录 探索Python性能监控的瑞士军刀&#xff1a;psutil的神秘面纱背景&#xff1a;为何psutil不可或缺&#xff1f;什么是psutil&#xff1f;如何安装psutil&#xff1f;五个简单的库函数使用方法场景应用&#xff1a;psutil在实际开发中的妙用常见问题与解决方案总结 探索…

性价比高的开放式耳机?开放式耳机推荐

在开放式耳机市场中&#xff0c;有多个品牌的性价比表现较为突出。以下是一些性价比较高的开放式耳机品牌及其产品特点&#xff1a; 1.虹觅&#xff08;Holme&#xff09; 虹觅Fit2&#xff1a; 以其简约而不失精致的设计&#xff0c;首先吸引了众多目光。这款耳机采用可调节…

苹果M4芯片Mac全面曝光 或10月发布

彭博社的马克・古尔曼&#xff08;Mark Gurman&#xff09;发布博文&#xff0c;曝料称苹果内部正在测试 4 款采用 M4 芯片的 Mac 设备&#xff0c;有望今年秋季&#xff08;可能是 10 月&#xff09;发布。 古尔曼表示苹果计划今年升级 MacBook Pro、Mac mini 和 iMac 产品线&…

驱动:中断底半部 platform平台总线

中断底半部实现方法&#xff1a; 1. 软中断2. tasklet 3. workqueue 解释 workqueue和tasklet是Linux内核中用于处理中断后续任务的两种机制&#xff0c;它们在中断处理流程中扮演着重要的角色。下面是对它们的详细解释&#xff1a; Tasklet 定义与作用&#xff1a; Taskl…

Vue笔记总结(Xmind格式):第二天

Xmind鸟瞰图&#xff1a; 简单文字总结&#xff1a; vue知识总结&#xff1a; 创建vue脚手架&#xff1a; 1.安装Node.js&#xff1a;Vue CLI作为一个npm包&#xff0c;需要Node.js来安装和运行。 2.安装Vue CLI&#xff1a;cmd指令 npm install -g vue/cli 3.创…