【Linux】内核同步机制之等待队列和完成量

news2024/9/27 5:35:37

文章目录

  • 完成量和等待队列
    • 1. 等待队列
      • 1.1 基本元素
      • 1.2 等待队列的创建和初始化
      • 1.3 等待队列元素的创建和初始化
      • 1.4 添加和移除等待队列元素到等待队列
    • 2. 等待事件机制
    • 3. 等待队列唤醒
    • 4. 总结
      • 4.1 等待事件方式
      • 4.2 手动休眠方式
      • 4.3 借助内核封装函数,进行手动休眠
    • 5. 完成量
      • 5.1 完成量定义
      • 5.2 完成量初始化
      • 5.3 完成量的使用

完成量和等待队列

1. 等待队列

Ref:https://www.cnblogs.com/hueyxu/p/13745029.html

1.1 基本元素

等待队列以循环链表为基础结构,链表头和链表项分别为等待队列头和等待队列元素,分别用结构体
wait_queue_head_t wait_queue_entry_t描述(定义在 linux/wait.h
等待队列头

struct wait_queue_head {
    spinlock_t          lock;
    struct list_head    head;
};

typedef struct wait_queue_head wait_queue_head_t;

等待队列元素

typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);
int default_wake_function(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key);

/* wait_queue_entry::flags */
#define WQ_FLAG_EXCLUSIVE   0x01
#define WQ_FLAG_WOKEN       0x02
#define WQ_FLAG_BOOKMARK    0x04

/*
 * A single wait-queue entry structure:
 */
struct wait_queue_entry {
    unsigned int        flags;    //标识队列元素状态和属性
    void                *private; // 用于指向关联进程task_struct结构体的指针
    wait_queue_func_t   func;     //函数指针,用于指向等待队列被唤醒时的回调的唤醒函数
    struct list_head    entry;
};

typedef struct wait_queue_entry wait_queue_entry_t;

 以进程阻塞和唤醒的过程为例,等待队列的使用场景可以简述为:
  进程 A 因等待某些资源(依赖进程 B 的某些操作)而不得不进入阻塞状态,便将当前进程加入到等待队列 Q 中。进程 B 在一系列操作后,可通知进程 A 所需资源已到位,便调用唤醒函数 wake up 来唤醒等待队列上 Q 的进程,注意此时所有等待在队列 Q 上的进程均被置为可运行状态。
借助上述描述场景,说明等待队列元素属性 flags 标志的作用,下文也将结合源码进行详细解读。

  1. WQ_FLAG_EXCLUSIVE
     上述场景中看到,当某进程调用wake up函数唤醒等待队列时,队列上所有的进程均被唤醒,在某些场合会出现唤醒的所有进程中,只有某个进程获得了期望的资源,而其他进程由于资源被占用不得不再次进入休眠。如果等待队列中进程数量庞大时,该行为将影响系统性能。
    内核增加了"独占等待”(WQ_FLAG_EXCLUSIVE)来解决此类问题。一个独占等待的行为和通常的休眠类似,但有如下两个重要的不同:
  • 等待队列元素设置 WQ_FLAG_EXCLUSIVE 标志时,会被添加到等待队列的尾部,而非头部。
  • 在某等待队列上调用 wake up 时,执行独占等待的进程每次只会唤醒其中第一个(所有非独占等待进程仍会被同时唤醒)。
  1. WQ_FLAG_WOKEN
     这是内部使用的标志,调用者不应该使用它
  2. WQ_FLAG_BOOKMARK
     用于wake_up()唤醒等待队列时实现分段遍历,减少单次对自旋锁的占用时间。

1.2 等待队列的创建和初始化

#define init_waitqueue_head(wq_head)                            \
    do {                                                        \
        static struct lock_class_key __key;                     \
        __init_waitqueue_head((wq_head), #wq_head, &__key);     \
    } while (0)

void __init_waitqueue_head(struct wait_queue_head *wq_head, const char *name, struct lock_class_key *key)
{
    spin_lock_init(&wq_head->lock);
    lockdep_set_class_and_name(&wq_head->lock, key, name);
    INIT_LIST_HEAD(&wq_head->head);
}

OR

#define DECLARE_WAIT_QUEUE_HEAD(name)                       \
    struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {               \
    .lock       = __SPIN_LOCK_UNLOCKED(name.lock),          \
    .head       = { &(name).head, &(name).head } }

1.3 等待队列元素的创建和初始化

//tsk -> 可指定非当前进程

#define DECLARE_WAITQUEUE(name, tsk)                        \
    struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)

#define __WAITQUEUE_INITIALIZER(name, tsk) {                \
    .private    = tsk,                                      \
    .func       = default_wake_function,                    \
    .entry      = { NULL, NULL } }

OR

//都指定当前进程

#define DEFINE_WAIT(name)   DEFINE_WAIT_FUNC(name, autoremove_wake_function)

#define DEFINE_WAIT_FUNC(name, function)                    \
    struct wait_queue_entry name = {                        \
        .private    = current,                              \
        .func       = function,                             \
        .entry      = LIST_HEAD_INIT((name).entry),         \
    }

#define init_wait(wait)                                     \
    do {                                                    \
        (wait)->private = current;                          \
        (wait)->func = autoremove_wake_function;            \
        INIT_LIST_HEAD(&(wait)->entry);                     \
        (wait)->flags = 0;                                  \
    } while (0)

1.4 添加和移除等待队列元素到等待队列

添加

add_wait_queue():在等待队列头部添加普通的等待队列元素(非独占等待,清除WQ_FLAG_EXCLUSIVE标志)。

void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    // 清除WQ_FLAG_EXCLUSIVE标志
    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}   

static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add(&wq_entry->entry, &wq_head->head);
}
add_wait_queue_exclusive():在等待队列尾部添加独占等待队列元素(设置了WQ_FLAG_EXCLUSIVE标志)
 
void add_wait_queue_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    // 设置WQ_FLAG_EXCLUSIVE标志
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue_entry_tail(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void __add_wait_queue_entry_tail(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_add_tail(&wq_entry->entry, &wq_head->head);
}

移除

remove_wait_queue()函数用于将等待队列元素 wq_entry从等待队列wq_head中移除。
 
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    unsigned long flags;

    spin_lock_irqsave(&wq_head->lock, flags);
    __remove_wait_queue(wq_head, wq_entry);
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
    list_del(&wq_entry->entry);
}

在这里插入图片描述

2. 等待事件机制

等待事件机制封装了等待队列和等待元素的初始化及两者之间插入和移除操作
内核中提供了等待事件wait_event()宏(以及它的几个变种),可用于实现简单的进程休眠,等待直至某个条件成立,主要包括如下几个定义:

wait_event(wq_head, condition)
wait_event_timeout(wq_head, condition, timeout) 
wait_event_interruptible(wq_head, condition)
wait_event_interruptible_timeout(wq_head, condition, timeout)
io_wait_event(wq_head, condition)

上述所有形式函数中, wq_head 是等待队列头(采用 值传递 的方式传输函数), condition 是任意一个布尔表达式。使用 wait_event ,进程将被置于非中断休眠,而使用 wait_event_interruptible 时,进程可以被信号中断。另外两个版本 wait_event_timeout 和 wait_event_interruptible_timeout 会使进程只等待限定的时间(以jiffy表示,给定时间到期时,宏均会返回0,而无论 condition 为何值)

API实现见Ref

3. 等待队列唤醒

等待事件机制向用户提供进程睡眠接口,等待队列唤醒向用户提供唤醒进程接口
前文已经简单提到,wake_up 函数可用于将等待队列上的所有进程唤醒,和 wait_event相对应wake_up 函数也包括多个变体。

wake_up(&wq_head)                        //唤醒等待队列上的所有进程
wake_up_interruptible(&wq_head)          //唤醒执行可中断休眠的进程
wake_up_nr(&wq_head, nr)                 //唤醒给定数目nr个独占等待进程,而非一个(wake_up函数唤醒一个)
wake_up_interruptible_nr(&wq_head, nr)   //唤醒给定数目nr个可中断的独占等待进程
wake_up_interruptible_all(&wq_head)      //唤醒等待队列上的所有可中断休眠独占等待进程

API实现见Ref

4. 总结

上述等待队列的使用方式有三种

4.1 等待事件方式

下面的程序展示了wait_event()wake_up()函数配合实现进程的休眠和唤醒流程
此方法只需要定义等待队列,等待队列元素将会在wait_event()函数中自动被创建、入等待队列、出等待队列

//声明
struct xxxx_touch {
    ...
    wait_queue_head_t   wait_queue;
    ...
}
//定义
static struct xxxx_touch xxxx_touch_dev = {
    ...
    .wait_queue = __WAIT_QUEUE_HEAD_INITIALIZER(xxxx_touch_dev.wait_queue),
    ...
}

//等待
static ssize_t p_sensor_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
    struct xxxx_touch_pdata *pdata = dev_get_drvdata(dev);
    struct xxxx_touch *touch_dev = pdata->device;

    wait_event_interruptible(touch_dev->wait_queue, pdata->psensor_changed);
    pdata->psensor_changed = false;

    return snprintf(buf, PAGE_SIZE, "%d\n", pdata->psensor_value);
}

//唤醒
int update_p_sensor_value(int value)
{
    struct xxxx_touch *dev = NULL;

    mutex_lock(&xxxx_touch_dev.psensor_mutex);

    if (!touch_pdata) {
        mutex_unlock(&xxxx_touch_dev.psensor_mutex);
        return -ENODEV;
    }

    dev = touch_pdata->device;

    if (value != touch_pdata->psensor_value) {
        MI_TOUCH_LOGI(1, "%s %s: value:%d\n", MI_TAG, __func__, value);
        touch_pdata->psensor_value = value;
        touch_pdata->psensor_changed = true;
        wake_up(&dev->wait_queue);
    }

    mutex_unlock(&xxxx_touch_dev.psensor_mutex);
    return 0;
}

4.2 手动休眠方式

DECLARE_WAIT_QUEUE_HEAD(queue);      //创建等待队列queue
DECLARE_WAITQUEUE(wait, current);    //创建等待队列元素,tsk为当前进程,wakefunc为default_wake_function


for (;;) {
    add_wait_queue(&queue, &wait);            //等待队列元素入队列
    set_current_state(TASK_INTERRUPTIBLE);    //设定当前进程可被外部信号唤醒
    if (condition)                            //如果条件成立出循环
        break;
    schedule();                               //调用调度器,让出CPU,进程休眠
    remove_wait_queue(&queue, &wait);         //被唤醒,等待队列结束,将等待队列元素出队列
    if (signal_pending(current))              //检查当前进程是否有信号处理,返回不为0表示有信号需要处理
        return -ERESTARTSYS;                  //如果有信号需要处理,则触发系统调用
}
set_current_state(TASK_RUNNING);              //设定当前进程运行态
remove_wait_queue(&queue, &wait);             //等待队列元素出队列

4.3 借助内核封装函数,进行手动休眠

DELARE_WAIT_QUEUE_HEAD(queue);    //创建等待队列queue
DEFINE_WAIT(wait);                //创建等待队列元素,tsk为当前进程,wakefunc为autoremove_wake_function


while (! condition) {
    prepare_to_wait(&queue, &wait, TASK_INTERRUPTIBLE);//实现见Ref
    if (! condition)
        schedule();
    finish_wait(&queue, &wait)                         //实现见Ref
}

5. 完成量

这是一个基于等待队列实现的内核同步机制,可以视为等待队列的一种封装
Ref: http://blog.chinaunix.net/uid-29680694-id-5768610.html

5.1 完成量定义

struct completion {
        unsigned int done;               //非零则条件满足不需要等待
        struct swait_queue_head wait;    //等待队列
};

5.2 完成量初始化

静态初始化

/**
 * DECLARE_COMPLETION - declare and initialize a completion structure
 * @work:  identifier for the completion structure
 *
 * This macro declares and initializes a completion structure. Generally used
 * for static declarations. You should use the _ONSTACK variant for automatic
 * variables.
 */
#define DECLARE_COMPLETION(work) \
        struct completion work = COMPLETION_INITIALIZER(work)
👇
#define COMPLETION_INITIALIZER(work) \
        { 0, __SWAIT_QUEUE_HEAD_INITIALIZER((work).wait) }
👇
#define __SWAIT_QUEUE_HEAD_INITIALIZER(name) {                                \
        .lock                = __RAW_SPIN_LOCK_UNLOCKED(name.lock),                \
        .task_list        = LIST_HEAD_INIT((name).task_list),                \
}

动态初始化

#define init_completion(x) __init_completion(x)

/**
 * init_completion - Initialize a dynamically allocated completion
 * @x:  pointer to completion structure that is to be initialized
 *
 * This inline function will initialize a dynamically created completion
 * structure.
 */
static inline void __init_completion(struct completion *x)
{
        x->done = 0;
        init_swait_queue_head(&x->wait);
}

5.3 完成量的使用

API

unsigned long __sched wait_for_completion_timeout(struct completion *x, unsigned long timeout);
void complete(struct completion *x);
static inline void reinit_completion(struct completion *x);

API 实现

/**
 * wait_for_completion_timeout: - waits for completion of a task (w/timeout)
 * @x:  holds the state of this particular completion
 * @timeout:  timeout value in jiffies
 *
 * This waits for either a completion of a specific task to be signaled or for a
 * specified timeout to expire. The timeout is in jiffies. It is not
 * interruptible.
 *
 * Return: 0 if timed out, and positive (at least 1, or number of jiffies left
 * till timeout) if completed.
 */
unsigned long __sched
wait_for_completion_timeout(struct completion *x, unsigned long timeout)
{
        return wait_for_common(x, timeout, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion_timeout);

/**
 * complete: - signals a single thread waiting on this completion
 * @x:  holds the state of this particular completion
 *
 * This will wake up a single thread waiting on this completion. Threads will be
 * awakened in the same order in which they were queued.
 *
 * See also complete_all(), wait_for_completion() and related routines.
 *
 * If this function wakes up a task, it executes a full memory barrier before
 * accessing the task state.
 */
void complete(struct completion *x)
{
        unsigned long flags;

        raw_spin_lock_irqsave(&x->wait.lock, flags);

        if (x->done != UINT_MAX)
                x->done++;
        swake_up_locked(&x->wait);
        raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete);

/**
 * reinit_completion - reinitialize a completion structure
 * @x:  pointer to completion structure that is to be reinitialized
 *
 * This inline function should be used to reinitialize a completion structure so it can
 * be reused. This is especially important after complete_all() is used.
 */
static inline void reinit_completion(struct completion *x)
{
        x->done = 0;
}

着重看下
其实还是等待队列那套套路
wait_for_completion_timeout

/**
 * wait_for_completion_timeout: - waits for completion of a task (w/timeout)
 * @x:  holds the state of this particular completion
 * @timeout:  timeout value in jiffies
 *
 * This waits for either a completion of a specific task to be signaled or for a
 * specified timeout to expire. The timeout is in jiffies. It is not
 * interruptible.
 *
 * Return: 0 if timed out, and positive (at least 1, or number of jiffies left
 * till timeout) if completed.
 */
unsigned long __sched
wait_for_completion_timeout(struct completion *x, unsigned long timeout)
{
        return wait_for_common(x, timeout, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion_timeout);
👇
static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
        return __wait_for_common(x, schedule_timeout, timeout, state);
}
👇
static inline long __sched
__wait_for_common(struct completion *x,
                  long (*action)(long), long timeout, int state)
{
        might_sleep();          //this func may sleep when user use it 

        complete_acquire(x);    // is NULL in 5.10

        raw_spin_lock_irq(&x->wait.lock);
        timeout = do_wait_for_common(x, action, timeout, state);
        raw_spin_unlock_irq(&x->wait.lock);

        complete_release(x);    // is NULL in 5.10

        return timeout;
}
👇
static inline long __sched
do_wait_for_common(struct completion *x,
                   long (*action)(long), long timeout, int state)
{
        if (!x->done) {
                DECLARE_SWAITQUEUE(wait); //swait_queue declare,tsk is current

                do {
                        if (signal_pending_state(state, current)) {
                                timeout = -ERESTARTSYS;
                                break;
                        }
                        __prepare_to_swait(&x->wait, &wait);//add swait_queue to swait_queue_head
                        __set_current_state(state);//set current state, here is TASK_UNINTERRUPTIBLE
                        raw_spin_unlock_irq(&x->wait.lock);//this action is sleep until timeout in here
                        timeout = action(timeout);
                        raw_spin_lock_irq(&x->wait.lock);
                } while (!x->done && timeout);
                __finish_swait(&x->wait, &wait);
                if (!x->done)
                        return timeout;
        }
        if (x->done != UINT_MAX)
                x->done--;
        return timeout ?: 1;
}

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

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

相关文章

前端开发_快应用开发

目录快应用官网真机调试组件组件嵌套问题tab组件list组件web组件css 样式问题[1]选择器[2]盒模型[3]样式布局-弹性布局[4-1]样式切换 - 类名的动态切换[4-2] 样式切换 - 行内样式动态切换[5]background[6]overflow[7]border-radius[8]盒子阴影[9] 单位系统接口[1] 检查某app是否…

机房运维6大隐患,你中了几个?

随着医院的看诊预约、缴费、打印报告等众多业务转至线上进行,对医院的网络及数据处理能力提出越来越高的要求,那么,机房的稳定、安全运行是医院网络信息系统的关键因素。 机房运维6大隐患 01.电源电力系统不稳定,网络设备运转遭到…

华为面试题就这?00后卷王直接拿下30k华为offer......

先说一下我的情况,某211本计算机,之前在深圳那边做了大约半年多少儿编程老师,之后内部平调回长沙这边,回来之后发现有点难,这边可能是业绩难做,虚假承诺很厉害,要给那些家长虚假承诺去骗人家&am…

红日(vulnstack)1 内网渗透ATTCK实战

环境准备 靶机链接:百度网盘 请输入提取码 提取码:sx22 攻击机系统:kali linux 2022.03 网络配置: win7配置: kali配置: kali 192.168.1.108 192.168.111.129 桥接一块,自定义网卡4 win7 1…

一文读懂云渲染“串流”全链路时延及优化策略

​这是一个让云游戏完美起步的时代。 云游戏作为产业内近年来炙手可热的话题,具有“云端运行、超高清、零延时、即点即玩”等众多特性。 随着 5G 时代的到来,以及中心云能力下沉至边缘云,高带宽、低延迟、高性能这些特性与云游戏紧密结合&a…

FastDDS-2.库概览

2. 库概览 Fast DDS(前身为Fast RTPS)是DDS规范的高效高性能实现,DDS规范是一种用于分布式应用软件的以数据为中心的通信中间件(DCPS)。本节回顾Fast DDS的体系结构、操作和关键特性。 2.1 架构 Fast DDS的架构如下图…

07_MySQL的单行函数

1. 函数的理解1.1 什么是函数函数在计算机语言的使用中贯穿始终,函数的作用是什么呢?它可以把我们经常使用的代码封装起来,需要的时候直接调用即可。这样既提高了代码效率 ,又提高了可维护性 。在 SQL 中我们也可以使用函数对检索…

python之wheel 包命名规则、abi 兼容和安装

一、windows安装python包,遇见的问题 1、python3以后的版本,安装python包,可以直接使用pip安装,但是安装时偶尔报错 2、安装python源码包,如何确定自己该安装哪个版本,一看就晕倒~~~(没人扶&…

PMP新考纲考试难不难,通过率怎样?

PMP考试难不难,还是因人而异的,对小白而言,肯定是难的,对项目管理老人而言,难度肯定是没那么高。 据考过的朋友讲,新考纲是有点难度的,尤其是最开始6月25日的考试,2023年就简单些了…

职场性别报告,男女薪酬仍有差距,男性平均薪酬比女性高29.7%

性别是否影响职业?女性求职比男性更加困难?男性薪酬比女性更有优势?人们一说到警察、建筑师通常会想到高大魁梧的男性形象,一说到幼师、护士往往想到的都是温柔的女性形象,职业好似与性别挂钩;女性求职通常…

OnlyOffice验证(二)在Centos7上部署OnlyOffice编译结果

在Centos7上部署OnlyOffice编译结果 此处将尝试将OnlyOffice验证(一)DocumentServer编译验证的结果部署到Centos7上。并且使用其它服务器现有的RabbitMq和Mysql。 安装Nginx 先安装Nginx需要的依赖环境: yum install openssl* -y yum insta…

java本地搭建宝塔部署实战likeadmin后台管理系统源码 - server端(一)

大家好啊,我是测评君,欢迎来到web测评。 上次分享了那几期likeadmin的搭建视频教程,由于是php开发的,这对有些习惯用java开发的同学不太友好,好在这套系统也有java版本的,也有朋友让我录制java版的搭建教程…

1. 驱动开发--基础知识

文章目录1 驱动的概念2 linux体系架构3 模块化设计3.1 微内核和宏内核4 linux设备驱动分类4.1 驱动分类4.2 三类驱动程序详细对比分析4.3 为什么字符设备驱动最重要5 驱动程序的安全性要求5.1 驱动是内核的一部分5.2 驱动对内核的影响5.3 常见驱动安全性问题6 驱动应该这么学6.…

「TCG 规范解读」词汇表

可信计算组织(Ttrusted Computing Group,TCG)是一个非盈利的工业标准组织,它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立,并采纳了由可信计算平台联盟(the Trusted Computing Platform Alli…

66岁“硅仙人与23岁“硅神童”创业晶圆厂,年薪17万美元

美国小伙打造出家庭芯片工厂! 溅射、氧化、光刻、蚀刻、绑定、封装全包括! 主人公Sam Zeloof,当时是一名大四学生。Sam Zeloof 是 YouTube和 Twitter 上的网络红人。多年来,他一直在记录自己在车库中制造硅芯片的令人印象深刻的…

BI的作用,体现在企业的哪些方面

对市场异常敏感的商业世界自然不会放过获取数字经济的机会,以国企和央企为首的众多企业开始进行数字化转型,通过信息化建设,部署商业智能BI来完成转型工作。 为什么会出现BI 有一点可能出乎很多人意料,虽然 BI 是因为信息化、数…

【项目实战】从0开始入门JDK源码 - ArrayList源码

一、源码位置 一般来说IDEA配置好JDK以后 ,JDK的源码其实也配置好了,本文是基于JDK1.8的源码说明 rt - java - util - ArrayList 二、 继承关系图 ArrayList是实现了 List接口的 public class ArrayList<E> extends AbstractList<E> implements

消息中间件的概念

中间件(middleware)是基础软件的一大类&#xff0c;属于可复用的软件范畴。中间件在操作系统软件&#xff0c;网络和数据库之上&#xff0c;应用软件之下&#xff0c;总的作用是为处于自己上层的应用软件提供运行于开发的环境&#xff0c;帮助用户灵活、高效的开发和集成复杂的…

Word控件Spire.Doc 【书签】教程(1):在C#/VB.NET:在 Word 中插入书签

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

用友YoSuite以高效增长的力量,助力春耕按下生产“快进键”

当下&#xff0c;ChatGPT和元宇宙是当前最热门的科技领域话题&#xff0c;这些高科技领域的发展让我们看到了人工智能与现实的无限可能。民以食为天&#xff0c;农业是国之根本&#xff0c;农业也是关系到每个人生活的重要领域。春耕备耕正当时&#xff0c;让我们拉回视野&…