redis7.x源码分析:(4) ae事件处理器(一)
ae模块是redis实现的Reactor模型的封装。它的主要代码实现集中在 ae.c 中,另外还提供了平台相关的io多路复用的封装,它们都实现了一套相同的poll接口,就类似于C++中提供了一个接口基类,由针对不同平台的派生类去实现。
// 创建平台相关的io模型实例
static int aeApiCreate(aeEventLoop *eventLoop)
// 修改可侦听的fd数量
static int aeApiResize(aeEventLoop *eventLoop, int setsize)
// 释放实例
static void aeApiFree(aeEventLoop *eventLoop)
// 添加或修改fd的侦听事件
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
// 删除或修改fd的侦听事件
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)
// 侦听所有fd的事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
// 返回使用的io模型名称
static char *aeApiName(void)
实际使用的io模型会根据编译时定义的宏在 ae.c 的代码头部直接引入。
/* Include the best multiplexing layer supported by this system.* The following should be ordered by performances, descending. */
// 根据不同操作系统下不同的宏选择应该使用的io复用模型
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else#ifdef HAVE_EPOLL// 对应linux#include "ae_epoll.c"#else#ifdef HAVE_KQUEUE// 对应BSD(FreeBSD、MacOS等)#include "ae_kqueue.c"#else// 对应Solaris#include "ae_select.c"#endif#endif
#endif
以linux为例先来看一下ae_epoll.c的实现,它的代码也比较简单:
static int aeApiCreate(aeEventLoop *eventLoop) {aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;// 分配events数组大小state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);if (!state->events) {zfree(state);return -1;}// 创建epoll, kernal 2.6.8 之后参数被忽略了state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */if (state->epfd == -1) {zfree(state->events);zfree(state);return -1;}anetCloexec(state->epfd);eventLoop->apidata = state;return 0;
}......static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0}; /* avoid valgrind warning *//* If the fd was already monitored for some event, we need a MOD* operation. Otherwise we need an ADD operation. */// 没注册过, 认为是添加fd到epollint op = eventLoop->events[fd].mask == AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;ee.events = 0;// 合并新老事件mask |= eventLoop->events[fd].mask; /* Merge old events */// 设置需要的事件if (mask & AE_READABLE) ee.events |= EPOLLIN;if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;ee.data.fd = fd;if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;return 0;
}static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0}; /* avoid valgrind warning */// 删除指定事件int mask = eventLoop->events[fd].mask & (~delmask);ee.events = 0;// 设置需要的事件if (mask & AE_READABLE) ee.events |= EPOLLIN;if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;ee.data.fd = fd;// 没有事件执行删除,否则执行修改if (mask != AE_NONE) {epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);} else {/* Note, Kernel < 2.6.9 requires a non null event pointer even for* EPOLL_CTL_DEL. */epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);}
}static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, numevents = 0;// 此处的 (tvp->tv_usec + 999)/1000 是为了避免当 tvp->tv_usec < 1000us 时转换成毫秒为 0ms, 导致epoll_wait不会等待// 因此这么写之后能保证只要 tvp->tv_usec 不为0,那么至少都能等待 1ms 时长retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);if (retval > 0) {int j;numevents = retval;for (j = 0; j < numevents; j++) {int mask = 0;struct epoll_event *e = state->events+j;if (e->events & EPOLLIN) mask |= AE_READABLE;if (e->events & EPOLLOUT) mask |= AE_WRITABLE;if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;// 保存触发的事件eventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = mask;}} else if (retval == -1 && errno != EINTR) {panic("aeApiPoll: epoll_wait, %s", strerror(errno));}return numevents;
}
接下来再看一下ae相关接口,先看下fd相关实现。
主要结构体定义如下:
/* File event structure */
typedef struct aeFileEvent {int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */ // 事件类型aeFileProc *rfileProc; // 读事件回调aeFileProc *wfileProc; // 写事件回调void *clientData; // 上下文指针
} aeFileEvent;/* Time event structure */
typedef struct aeTimeEvent {long long id; /* time event identifier. */ // 定时器事件idmonotime when; // 触发时间aeTimeProc *timeProc; // 定时器触发回调aeEventFinalizerProc *finalizerProc; // 定时器销毁时执行的回调void *clientData; // 上下文指针struct aeTimeEvent *prev; // 前一个定时器事件struct aeTimeEvent *next; // 后一个定时器事件int refcount; /* refcount to prevent timer events from being* freed in recursive time event calls. */ // 引用计数
} aeTimeEvent;/* A fired event */
typedef struct aeFiredEvent {int fd; // 触发事件的定时器int mask; // 触发的事件类型
} aeFiredEvent;/* State of an event based program */
typedef struct aeEventLoop {int maxfd; /* highest file descriptor currently registered */ // 注册的最大fd值int setsize; /* max number of file descriptors tracked */ // 注册的fd数量long long timeEventNextId; // 自增用于生成定时器idaeFileEvent *events; /* Registered events */ // 注册的fd事件aeFiredEvent *fired; /* Fired events */ // 触发的fd事件aeTimeEvent *timeEventHead; // 定时器事件链表头int stop; // 结束标志void *apidata; /* This is used for polling API specific data */ // 不同poll的上下文信息aeBeforeSleepProc *beforesleep; // poll前调用aeBeforeSleepProc *aftersleep; // poll后调用int flags;
} aeEventLoop;
aeEventLoop中的events是用于保存fd注册事件的数组,它是以fd值作为索引来存取事件的。
创建eventloop事件管理器:
aeEventLoop *aeCreateEventLoop(int setsize) {aeEventLoop *eventLoop;int i;// 初始化一下单调时钟使用哪种方式实现monotonicInit(); /* just in case the calling app didn't initialize */if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;// events、fired数组以fd值为下标,直接存取对应fd的相关数据eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;eventLoop->setsize = setsize;eventLoop->timeEventHead = NULL;eventLoop->timeEventNextId = 0;eventLoop->stop = 0;eventLoop->maxfd = -1;eventLoop->beforesleep = NULL;eventLoop->aftersleep = NULL;eventLoop->flags = 0;// 根据平台创建io模型实例if (aeApiCreate(eventLoop) == -1) goto err;/* Events with mask == AE_NONE are not set. So let's initialize the* vector with it. */for (i = 0; i < setsize; i++)eventLoop->events[i].mask = AE_NONE;return eventLoop;err:if (eventLoop) {zfree(eventLoop->events);zfree(eventLoop->fired);zfree(eventLoop);}return NULL;
}
添加和删除事件:
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{// 超出events数组有效范围直接报错if (fd >= eventLoop->setsize) {errno = ERANGE;return AE_ERR;}aeFileEvent *fe = &eventLoop->events[fd];// 添加或修改fd读写事件if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;fe->mask |= mask;if (mask & AE_READABLE) fe->rfileProc = proc;if (mask & AE_WRITABLE) fe->wfileProc = proc;fe->clientData = clientData;if (fd > eventLoop->maxfd)eventLoop->maxfd = fd;return AE_OK;
}void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{if (fd >= eventLoop->setsize) return;aeFileEvent *fe = &eventLoop->events[fd];if (fe->mask == AE_NONE) return;/* We want to always remove AE_BARRIER if set when AE_WRITABLE* is removed. */if (mask & AE_WRITABLE) mask |= AE_BARRIER;aeApiDelEvent(eventLoop, fd, mask);fe->mask = fe->mask & (~mask);// 如果本次删除事件的fd是最大的fd,并且该fd已经没有事件了(可以认为该fd被清除了)if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {/* Update the max fd */int j;// 从后往前遍历找到第一个有事件的fd赋给maxfdfor (j = eventLoop->maxfd-1; j >= 0; j--)if (eventLoop->events[j].mask != AE_NONE) break;eventLoop->maxfd = j;}
}
事件处理函数,处理一轮事件:
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{int processed = 0, numevents;/* Nothing to do? return ASAP */if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;/* Note that we want to call select() even if there are no* file events to process as long as we want to process time* events, in order to sleep until the next time event is ready* to fire. */if (eventLoop->maxfd != -1 ||((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {int j;struct timeval tv, *tvp;int64_t usUntilTimer = -1;// 获取最近的一个定时器距当前时间的触发时长if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))usUntilTimer = usUntilEarliestTimer(eventLoop);if (usUntilTimer >= 0) {tv.tv_sec = usUntilTimer / 1000000;tv.tv_usec = usUntilTimer % 1000000;tvp = &tv;} else {/* If we have to check for events but need to return* ASAP because of AE_DONT_WAIT we need to set the timeout* to zero */if (flags & AE_DONT_WAIT) {tv.tv_sec = tv.tv_usec = 0;tvp = &tv;} else {/* Otherwise we can block */tvp = NULL; /* wait forever */}}// 不需要等待,超时时间设为0if (eventLoop->flags & AE_DONT_WAIT) {tv.tv_sec = tv.tv_usec = 0;tvp = &tv;}// 执行poll前的回调(对fd读写以及命令处理都会在beforsleep中执行, 如果开启多线程读写也会在它内部一并处理)if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)eventLoop->beforesleep(eventLoop);/* Call the multiplexing API, will return only on timeout or when* some event fires. */// 等待事件numevents = aeApiPoll(eventLoop, tvp);/* After sleep callback. */// 执行poll后的回调if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)eventLoop->aftersleep(eventLoop);// 处理poll到的读写事件for (j = 0; j < numevents; j++) {int fd = eventLoop->fired[j].fd;aeFileEvent *fe = &eventLoop->events[fd];int mask = eventLoop->fired[j].mask;int fired = 0; /* Number of events fired for current fd. *//* Normally we execute the readable event first, and the writable* event later. This is useful as sometimes we may be able* to serve the reply of a query immediately after processing the* query.** However if AE_BARRIER is set in the mask, our application is* asking us to do the reverse: never fire the writable event* after the readable. In such a case, we invert the calls.* This is useful when, for instance, we want to do things* in the beforeSleep() hook, like fsyncing a file to disk,* before replying to a client. */// 是否反转读写事件的顺序(不设置 AE_BARRIER 时,先读后写)int invert = fe->mask & AE_BARRIER;/* Note the "fe->mask & mask & ..." code: maybe an already* processed event removed an element that fired and we still* didn't processed, so we check if the event is still valid.** Fire the readable event if the call sequence is not* inverted. */if (!invert && fe->mask & mask & AE_READABLE) {// 处理读事件回调fe->rfileProc(eventLoop,fd,fe->clientData,mask);fired++;// 防止在事件处理函数中 resize 过数组大小,所以需要重新获取下地址fe = &eventLoop->events[fd]; /* Refresh in case of resize. */}/* Fire the writable event. */if (fe->mask & mask & AE_WRITABLE) {// 处理写事件回调(读写回调函数相同时,每轮poll只执行一次读或者写回调)if (!fired || fe->wfileProc != fe->rfileProc) {fe->wfileProc(eventLoop,fd,fe->clientData,mask);fired++;}}/* If we have to invert the call, fire the readable event now* after the writable one. */if (invert) {// 反转读写时先写,然后在这边后读(读写回调函数相同时,每轮poll只执行一次读或者写回调)fe = &eventLoop->events[fd]; /* Refresh in case of resize. */if ((fe->mask & mask & AE_READABLE) &&(!fired || fe->wfileProc != fe->rfileProc)){fe->rfileProc(eventLoop,fd,fe->clientData,mask);fired++;}}processed++;}}/* Check time events */// 处理定时器事件if (flags & AE_TIME_EVENTS)processed += processTimeEvents(eventLoop);return processed; /* return the number of processed file/time events */
}
执行主事件循环:
void aeMain(aeEventLoop *eventLoop) {// 启动事件循环eventLoop->stop = 0;while (!eventLoop->stop) {// 处理事件aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);}
}
相关文章:
redis7.x源码分析:(4) ae事件处理器(一)
ae模块是redis实现的Reactor模型的封装。它的主要代码实现集中在 ae.c 中,另外还提供了平台相关的io多路复用的封装,它们都实现了一套相同的poll接口,就类似于C中提供了一个接口基类,由针对不同平台的派生类去实现。 // 创建平台…...

【React】React Router:深入理解前端路由的工作原理
🌈个人主页: 鑫宝Code 🔥热门专栏: 闲话杂谈| 炫酷HTML | JavaScript基础 💫个人格言: "如无必要,勿增实体" 文章目录 React Router:深入理解前端路由的工作原理路由的演进历程传统多页面…...

51单片机-独立按键与数码管联动
独立键盘和矩阵键盘检测原理及实现 键盘的分类:编码键盘和非编码键盘 键盘上闭合键的识别由专用的硬件编码器实现,并产生键编码号或键值的称为编码键盘,如:计算机键盘。靠软件编程识别的称为非编码键盘;在单片机组成…...
visual studio 2005的MFC各种线程函数之间的调用关系
在 Visual Studio 2005 的 MFC 程序中的函数和消息机制涉及线程间通信、消息处理以及与窗口消息的交互。接下来我将详细分析以下每个函数的作用、如何使用它们以及它们之间的调用关系。 1. PostThreadMessage(m_iThOpID, MSG_OP_OVER, 0, (LPARAM)iLparm); 函数用途࿱…...

网页中调用系统的EXE文件,如打开QQ
遇到一个实际的问题,需要在网页中打开本地的某个工业软件。 通过点击exe文件就可以调用到程序。 比如双击qq的exe就可以启动qq的程序。 那么问题就变成了如何加载exe程序呢? 可以通过Java的 Process process Runtime.getRuntime().exec(command);通过…...

【单点知识】基于PyTorch讲解自动编码器(Autoencoder)
文章目录 0. 前言1. 自动编码器的基本概念1.1 定义1.2 目标1.3 结构 2. PyTorch实现自动编码器2.1 导入必要的库2.2 定义自动编码器模型2.3 加载数据2.4 训练自动编码器 3. 自动编码器的意义4. 自动编码器的应用4.1 图像处理4.2自然语言处理:4.3推荐系统:…...

Halo 正式开源: 使用可穿戴设备进行开源健康追踪
在飞速发展的可穿戴技术领域,我们正处于一个十字路口——市场上充斥着各式时尚、功能丰富的设备,声称能够彻底改变我们对健康和健身的方式。 然而,在这些光鲜的外观和营销宣传背后,隐藏着一个令人担忧的现实:大多数这些…...
summernote富文本批量上传音频,视频等附件
普通项目,HTML的summernote富文本批量上传音频,视频等附件(其他附件同理) JS和CSS的引入 <head><th:block th:include"include :: summernote-css" /> </head> <body><th:block th:include"include :: summernote-js" /> …...

IDEA如何设置编码格式,字符编码,全局编码和项目编码格式
前言 大家好,我是小徐啊。我们在开发Java项目(Springboot)的时候,一般都是会设置好对应的编码格式的。如果设置的不恰当,容易造成乱码的问题,这是要避免的。今天,小徐就来介绍下我们如何在IDEA…...

【计算机网络实验】之静态路由配置
【计算机网络实验】之静态路由配置 实验题目实验目的实验任务实验设备实验环境实验步骤路由器配置设置静态路由测试路由器之间的连通性配置主机PC的IP测试 实验题目 静态路由协议的配置 实验目的 熟悉路由器工作原理和机制;巩固静态路由理论;设计简单…...
十五届蓝桥杯赛题-c/c++ 大学b组
握手问题 很简单,相互牵手即可,但是要注意,第一个人只能与其他49个人牵手,所以开头是加上49 #include <iostream> using namespace std; int main() {int cnt0;for(int i49;i>7;i--){cnti;//cout<<i<<&quo…...
基础自动化系统的任务
基础自动化系统的任务主要包括实现自动控制、提高生产效率、减少人工干预等。以下是其具体任务的相关介绍: 实现自动控制 控制机器设备:基础自动化系统通过预设的程序和逻辑规则,对机器或设备进行自动控制和运行。执行特定任务:这…...

DBeaver添加地图查看器的自定义底图
DBeaver提供了空间数据在地图上查看的功能,地图查看器技术上基于Leaflet实现。 当我们在表格中选择图形列时,空间数据会叠加在右侧的地图查看器上。 其本质是在缓存中会生成一个静态页面,点击查看器左下角的“在浏览器中打开”,可…...

STM32F103C8T6实时时钟RTC
目录 前言 一、RTC基本硬件结构 二、Unix时间戳 2.1 unix时间戳定义 2.2 时间戳与日历日期时间的转换 2.3 指针函数使用注意事项 三、RTC和BKP硬件结构 四、驱动代码解析 前言 STM32F103C8T6外部低速时钟LSE(一般为32.768KHz)用的引脚是PC14和PC…...
Python Selenium:Web自动化测试与爬虫开发
Python Selenium:Web自动化测试与爬虫开发 Python Selenium:Web自动化测试与爬虫开发安装Selenium设置WebDriver基础示例页面元素交互处理JavaScript和Cookies浏览器控制屏幕截图Headless Mode结束会话错误处理与调试 ***本文由AI辅助生成*** Python Se…...

Java-07 深入浅出 MyBatis - 一对多模型 SqlMapConfig 与 Mapper 详细讲解测试
点一下关注吧!!!非常感谢!!持续更新!!! 大数据篇正在更新!https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了: MyBatisÿ…...

用CAXA CAD电子图板导入图框、标题栏并导出pdf的方法
1.导入图框: 点击调入图框->出现读入图框文件 一个一个点击,选择合适的图框 然后点击导入 2.导入标题栏: 调入标题栏->出现读入标题栏文件 一个一个点击,选择合适的标题栏,然后点击导入 3.导出pdf&#x…...

深入了解 Linux htop 命令:功能、用法与示例
文章目录 深入了解 Linux htop 命令:功能、用法与示例什么是 htop?htop 的安装htop的基本功能A区:系统资源使用情况B区:系统概览信息C区:进程列表D区:功能键快捷方式 与 top 的对比常见用法与示例实际场景应…...
JDK1.8新增特性
新特性: Lambda表达式: (语法三要素:参数、箭头、代码) JDK1.8引入的一种新语法Lambda表达式,它简化了匿名内部类的使用和提高代码的可读性。 /**正常写法创建Runable**/ Runnable runnable new Runnable() {Overridepublic voi…...

环境背景文本到语音转换
目录 概述演示效果核心逻辑使用方式 概述 本文所涉及的所有资源的获取方式:https://www.aspiringcode.com/content?id100000000027&uid2f1061526e3a4548ab2e111ad079ea8c 论文标题: 本文提出了 VoiceLDM,这是一种旨在生成准确遵循两种…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...