当前位置: 首页 > news >正文

高并发线程池设计Nginx线程池源码剖析

为什么我们需要线程池?Why?

省流:

为了解决:

1.访问磁盘速度慢

2.等待设备工作

3.....

我们使用多线程技术,在IO繁忙的时候优先处理别的任务

为了解决多线程的缺陷:

1.创建、销毁线程时间消耗大

2.创建线程太多使系统资源不足或者线程频繁切换耗时太多

我们采用了线程池技术

线程池

两个主要组成部分

任务队列(类比银行客户)、线程池(类比银行窗口)

工作流程

1.工作进程碰到需要"阻塞"的操作

2.将操作放进任务队列

3.空闲进程处理任务队列中的任务

核心组件 

 

Nginx线程池源码剖析 

thread_pool.h

完成任务、任务队列、线程池的结构定义

#ifndef _THREAD_POOL_H_INCLUDED_
#define _THREAD_POOL_H_INCLUDED_#include "thread.h"#define DEFAULT_THREADS_NUM 4 //默认线程数
#define DEFAULT_QUEUE_NUM  65535 //默认任务队列长度typedef unsigned long         atomic_uint_t;
typedef struct thread_task_s  thread_task_t;//线程任务
typedef struct thread_pool_s  thread_pool_t;//线程池struct thread_task_s {thread_task_t       *next;uint_t               id;//标识void                *ctx;//上下文 指向执行函数参数的起始地址void               (*handler)(void *data);//指向空闲线程要执行的函数
};typedef struct {//单链表结构thread_task_t        *first;thread_task_t        **last;
} thread_pool_queue_t;//任务队列//利用宏函数进行初始化
#define thread_pool_queue_init(q)                                         \(q)->first = NULL;                                                    \(q)->last = &(q)->firststruct thread_pool_s {pthread_mutex_t        mtx;//线程池的互斥锁thread_pool_queue_t   queue;//任务队列int_t                 waiting;//任务队列中的任务数pthread_cond_t         cond;//条件变量char                  *name;//线程池名字uint_t                threads;//线程数量int_t                 max_queue;//最大队列长度
};thread_task_t *thread_task_alloc(size_t size);
int_t thread_task_post(thread_pool_t *tp, thread_task_t *task);
thread_pool_t* thread_pool_init();
void thread_pool_destroy(thread_pool_t *tp);#endif /* _THREAD_POOL_H_INCLUDED_ */

thread_pool.c

thread_pool_init函数——线程池初始化

作用:线程池初始化

thread_pool_t* thread_pool_init()
{int             err;pthread_t       tid;uint_t          n;pthread_attr_t  attr;thread_pool_t   *tp=NULL;tp = calloc(1,sizeof(thread_pool_t));if(tp == NULL){fprintf(stderr, "thread_pool_init: calloc failed!\n");}thread_pool_init_default(tp, NULL);thread_pool_queue_init(&tp->queue);if (thread_mutex_create(&tp->mtx) != OK) {free(tp);return NULL;}if (thread_cond_create(&tp->cond) != OK) {(void) thread_mutex_destroy(&tp->mtx);free(tp);return NULL;}err = pthread_attr_init(&attr);if (err) {fprintf(stderr, "pthread_attr_init() failed, reason: %s\n",strerror(errno));free(tp);return NULL;}err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);if (err) {fprintf(stderr, "pthread_attr_setdetachstate() failed, reason: %s\n",strerror(errno));free(tp);return NULL;}for (n = 0; n < tp->threads; n++) {err = pthread_create(&tid, &attr, thread_pool_cycle, tp);if (err) {fprintf(stderr, "pthread_create() failed, reason: %s\n",strerror(errno));free(tp);return NULL;}}(void) pthread_attr_destroy(&attr);return tp;
}

剖析

tp = calloc(1,sizeof(thread_pool_t));

为线程池分配内存

thread_pool_init_default(tp, NULL);

执行默认的初始化(设置默认线程数、任务队列最大长度、线程池名字)

thread_pool_init_default代码如下:

static int_t
thread_pool_init_default(thread_pool_t *tpp, char *name)
{if(tpp){tpp->threads = DEFAULT_THREADS_NUM;//设置默认线程数tpp->max_queue = DEFAULT_QUEUE_NUM;//设置任务队列最大长度tpp->name = strdup(name?name:"default");//设置线程名字if(debug)fprintf(stderr,"thread_pool_init, name: %s ,threads: %lu max_queue: %ld\n",tpp->name, tpp->threads, tpp->max_queue);return OK;}return ERROR;
}

回到thread_pool_init:

thread_pool_queue_init(&tp->queue);

用宏函数初始化任务队列

回到thread_pool_init:

创建互斥锁和条件变量,代码如下

if (thread_mutex_create(&tp->mtx) != OK) {free(tp);return NULL;
}if (thread_cond_create(&tp->cond) != OK) {(void) thread_mutex_destroy(&tp->mtx);free(tp);return NULL;
}

对线程属性进行初始化,在线程创建时将其属性设为分离态(detached):父线程使用thread_join 无法等待到结束了的子进程,代码如下:

 err = pthread_attr_init(&attr);if (err) {fprintf(stderr, "pthread_attr_init() failed, reason: %s\n",strerror(errno));free(tp);return NULL;}err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);if (err) {fprintf(stderr, "pthread_attr_setdetachstate() failed, reason: %s\n",strerror(errno));free(tp);return NULL;}

创建线程并消费属性变量(属性已经设置到线程中了,属性变量就没用了),为每个线程设置的任务为thread_pool_cycle(下面介绍)

 for (n = 0; n < tp->threads; n++) {err = pthread_create(&tid, &attr, thread_pool_cycle, tp);if (err) {fprintf(stderr, "pthread_create() failed, reason: %s\n",strerror(errno));
free(tp);return NULL;}}(void) pthread_attr_destroy(&attr);

thread_pool_cycle函数——每个线程的生命周期

完整代码

static void *
thread_pool_cycle(void *data)
{thread_pool_t *tp = data;int                 err;thread_task_t       *task;if(debug)fprintf(stderr,"thread in pool \"%s\" started\n", tp->name);for ( ;; ) {if (thread_mutex_lock(&tp->mtx) != OK) {return NULL;}tp->waiting--;while (tp->queue.first == NULL) {if (thread_cond_wait(&tp->cond, &tp->mtx)!= OK){(void) thread_mutex_unlock(&tp->mtx);return NULL;}}task = tp->queue.first;tp->queue.first = task->next;if (tp->queue.first == NULL) {tp->queue.last = &tp->queue.first;}if (thread_mutex_unlock(&tp->mtx) != OK) {return NULL;}if(debug) fprintf(stderr,"run task #%lu in thread pool \"%s\"\n",task->id, tp->name);task->handler(task->ctx);if(debug) fprintf(stderr,"complete task #%lu in thread pool \"%s\"\n",task->id, tp->name);task->next = NULL;//notify }
}

剖析

进入for循环,尝试对任务队列上锁(因为任务队列是临界资源,访问任务队列前要上锁)

代码如下:

for ( ;; ) {if (thread_mutex_lock(&tp->mtx) != OK) {return NULL;}

线程拿到锁之后,使得等待任务数量-1,并调用thread_con_wait函数等待条件变量,并挂起进程,代码如下:

while (tp->queue.first == NULL) {if (thread_cond_wait(&tp->cond, &tp->mtx)!= OK){(void) thread_mutex_unlock(&tp->mtx);return NULL;}
}
thread_pool_cycle和thread_task_post的同步机制cle

thread_cond_wait执行之后,线程会被挂起,并且对tp->mtx进行解锁;

那么什么时候将线程唤醒呢?——在thread_task_post中

thread_task_post的作用是将任务挂载到任务队列中,thread_task_post首先会对线程池进行上锁,然后对task进行初始化,然后唤醒等待条件变量的线程,注意此时cycle线程还不能往下执行,因为thread_task_post已经获得了锁,只有在thread_task_post将任务挂载到队列上并解锁后,cycle才能从thread_cond_wati往下执行。

thread_task_post代码如下:

int_t
thread_task_post(thread_pool_t* tp, thread_task_t* task)
{if (thread_mutex_lock(&tp->mtx) != OK) {return ERROR;}/*此部分对任务属性进行初始化*/if (thread_cond_signal(&tp->cond) != OK) {(void)thread_mutex_unlock(&tp->mtx);return ERROR;}//将任务挂在到任务队列上*tp->queue.last = task;tp->queue.last = &task->next;tp->waiting++;(void)thread_mutex_unlock(&tp->mtx);//对线程池进行解锁/*函数返回*/}

 

回到thread_pool_init剩下的代码:

task = tp->queue.first;
tp->queue.first = task->next;if (tp->queue.first == NULL) {tp->queue.last = &tp->queue.first;
}if (thread_mutex_unlock(&tp->mtx) != OK) {return NULL;
}if(debug) fprintf(stderr,"run task #%lu in thread pool \"%s\"\n",task->id, tp->name);task->handler(task->ctx);if(debug) fprintf(stderr,"complete task #%lu in thread pool \"%s\"\n",task->id, tp->name);task->next = NULL;//notify 

获取任务队列中的任务后进行解锁 (从thread_cond_wati返回时进行了解锁),执行函数handler,结束.....

thread_task_alloc函数——为任务分配内存

作用:为任务分配内存,内存包括两部分:任务结构体所占内存、任务函数的参数所占内存。

thread_task_t *
thread_task_alloc(size_t size)
{thread_task_t  *task;task = calloc(1,sizeof(thread_task_t) + size);if (task == NULL) {return NULL;}task->ctx = task + 1;//ctx指向任务执行函数参数的起始地址return task;
}

传入的参数size用来指定任务的执行函数的参数的大小

task = calloc(1,sizeof(thread_task_t) + size);

为任务分配内存,内存大小为任务结构体大小+参数的大小

task->ctx = task + 1;//tast->ctx指向任务函数参数的起始地址

task指针指向的是任务结体存储的起始地址,task+1表示向下偏移一个thread_task_t的长度,

也就是说task+1指向的是函数变量的起始地址,如图所示

创建任务并传参示例
#include "thread_pool.h"struct test {int arg1;int arg2;
};void task_handler3(void* data) {static int index = 0;struct test* t = (struct test*)data;printf("Hello, this is 3th test.index=%d\r\n", index++);printf("arg1: %d, arg2: %d\n", t->arg1, t->arg2);}int
main(int argc, char** argv)
{thread_pool_t* tp = NULL;int i = 0;tp = thread_pool_init();//线程池初始化//sleep(1);thread_task_t* test3 = thread_task_alloc(sizeof(struct test));//给任务分配内存test3->handler = task_handler3;//为任务设置执行函数((struct test*)test3->ctx)->arg1 = 666;//设置参数值((struct test*)test3->ctx)->arg2 = 888;}

thread_task_post函数——往线程池中投递任务

int_t
thread_task_post(thread_pool_t *tp, thread_task_t *task)
{if (thread_mutex_lock(&tp->mtx) != OK) {return ERROR;}if (tp->waiting >= tp->max_queue) {(void) thread_mutex_unlock(&tp->mtx);fprintf(stderr,"thread pool \"%s\" queue overflow: %ld tasks waiting\n",tp->name, tp->waiting);return ERROR;}//task->event.active = 1;task->id = thread_pool_task_id++;task->next = NULL;if (thread_cond_signal(&tp->cond) != OK) {(void) thread_mutex_unlock(&tp->mtx);return ERROR;}*tp->queue.last = task;tp->queue.last = &task->next;tp->waiting++;(void) thread_mutex_unlock(&tp->mtx);if(debug)fprintf(stderr,"task #%lu added to thread pool \"%s\"\n",task->id, tp->name);return OK;
}

代码剖析

if (thread_mutex_lock(&tp->mtx) != OK) {return ERROR;
}

线程池是临界资源,分配任务前要对线程池上锁。

if (tp->waiting >= tp->max_queue) {(void) thread_mutex_unlock(&tp->mtx);fprintf(stderr,"thread pool \"%s\" queue overflow: %ld tasks waiting\n",tp->name, tp->waiting);return ERROR;
}

线程池中等待任务数量>=任务队列长度,表明任务队列已满,则对任务队列进行解锁并报错

task->id = thread_pool_task_id++;
task->next = NULL;

为任务分配唯一id,thread_pool_task_id是一个全局的静态变量

if (thread_cond_signal(&tp->cond) != OK) {(void) thread_mutex_unlock(&tp->mtx);return ERROR;
}

这里会设置条件变量,唤醒处于thread_con_wait状态的线程池中的线程

 

*tp->queue.last = task;
tp->queue.last = &task->next;tp->waiting++;

任务队列结构如下:

tp->queue.last是一个二级指针,表示的是队尾节点的指针的地址

通过这个二级指针可以很快地在队尾插入结点,如图所示

wating表示任务队列中等待的任务数目,使其+1

(void) thread_mutex_unlock(&tp->mtx);if(debug)fprintf(stderr,"task #%lu added to thread pool \"%s\"\n",task->id, tp->name);

解锁线程池的锁,debug打印信息,没什么好说的。

thread_mutex_create函数——对pthread_mutex_init函数进行封装

int
thread_mutex_create(pthread_mutex_t *mtx)
{int            err;pthread_mutexattr_t  attr;err = pthread_mutexattr_init(&attr);if (err != 0) {fprintf(stderr, "pthread_mutexattr_init() failed, reason: %s\n",strerror(errno));return ERROR;}err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);if (err != 0) {fprintf(stderr, "pthread_mutexattr_settype(PTHREAD_MUTEX_ERRORCHECK) failed, reason: %s\n",strerror(errno));return ERROR;}err = pthread_mutex_init(mtx, &attr);if (err != 0) {fprintf(stderr,"pthread_mutex_init() failed, reason: %s\n",strerror(errno));return ERROR;}err = pthread_mutexattr_destroy(&attr);if (err != 0) {fprintf(stderr,"pthread_mutexattr_destroy() failed, reason: %s\n",strerror(errno));}return OK;
}

在pthread_mutex_init的基础上加了错误检查和一些属性的设定。

剖析

err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK);
if (err != 0) {fprintf(stderr, "pthread_mutexattr_settype(PTHREAD_MUTEX_ERRORCHECK) failed, reason: %s\n",strerror(errno));return ERROR;
}

PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD MUTEX TIMED NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。

PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。

thread_pool_destroy函数——摧毁线程池

功能:当程序运行将要结束时,摧毁线程池,对资源进行释放。

完整代码

void thread_pool_destroy(thread_pool_t *tp)
{uint_t           n;thread_task_t    task;volatile uint_t  lock;memset(&task,'\0', sizeof(thread_task_t));task.handler = thread_pool_exit_handler;task.ctx = (void *) &lock;for (n = 0; n < tp->threads; n++) {lock = 1;if (thread_task_post(tp, &task) != OK) {return;}while (lock) {sched_yield();}//task.event.active = 0;}(void) thread_cond_destroy(&tp->cond);(void) thread_mutex_destroy(&tp->mtx);free(tp);
}

给线程池中的每一个线程分配一个“自杀”的任务,同时调用sched_yield降低执行thread_pool_destroy函数的线程的优先级。

等分配完"自杀"任务之后,释放条件变量、互斥锁和给线程池分配的内存,线程池就被摧毁了。

“自杀”函数如下:

static void
thread_pool_exit_handler(void *data)
{uint_t *lock = data;*lock = 0;pthread_exit(0);
}

只有自杀函数执行到*lock=0时,执行thread_pool_destroy函数的线程才能给下一个线程分配"自杀"任务,否则就一直执行sched_yield函数,让出CPU时间片。

相关文章:

高并发线程池设计Nginx线程池源码剖析

为什么我们需要线程池?Why? 省流&#xff1a; 为了解决: 1.访问磁盘速度慢 2.等待设备工作 3..... 我们使用多线程技术&#xff0c;在IO繁忙的时候优先处理别的任务 为了解决多线程的缺陷: 1.创建、销毁线程时间消耗大 2.创建线程太多使系统资源不足或者线程频繁切换…...

SEO:6个避免被搜索引擎惩罚的策略-华媒舍

在当今数字时代&#xff0c;搜索引擎成为了绝大多数人获取信息和产品的首选工具。为了在搜索结果中获得良好的排名&#xff0c;许多网站采用了各种优化策略。有些策略可能会适得其反&#xff0c;引发搜索引擎的惩罚。以下是彭博社发稿推广的6个避免被搜索引擎惩罚的策略。 1. 内…...

STM32之六:SysTick系统滴答定时器

目录 1. SysTick简介 2. 时钟来源 3. SysTick寄存器 3.1 CTRL—SysTick控制及状态寄存器 3.2 RELOAD—SysTick重装载数值寄存器 3.3 CURRENT—SysTick当前数值寄存器 4. systick系统定时器配置 5. 延时函数实现 5.1 延时函数编写步骤 5.2 微秒级延时函数delay_us 5.…...

全栈物联网项目:结合 C/C++、Python、Node.js 和 React 开发智能温控系统(附代码示例)

1. 项目概述 本文详细介绍了一个基于STM32微控制器和AWS IoT云平台的智能温控器项目。该项目旨在实现远程温度监控和控制,具有以下主要特点: 使用STM32F103微控制器作为主控芯片,负责数据采集、处理和控制逻辑采用DHT22数字温湿度传感器,精确采集环境温湿度数据通过ESP8266 W…...

WPF学习(3) -- 控件模板

一、操作过程 二、代码 <Window x:Class"学习.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d"http://schemas.microsoft.com/expressio…...

Netty Websocket SpringBoot Starter

netty websocket starter Quick Start Demo 项目 添加依赖 <!--添加源--> <repository><id>github</id><url>https://maven.pkg.github.com</url><snapshots><enabled>true</enabled></snapshots> </reposit…...

数据结构(4.2)——朴素模式匹配算法

字符串模式匹配 在主串中找到模式串相同的子串&#xff0c;并返回其所在的位置。 子串和模式串的区别 子串&#xff1a;主串的一部分&#xff0c;一定存在 模式串&#xff1a;不一定能在主串中找到 字符串模式匹配 朴素模式匹配算法 主串长度为n&#xff0c;模式串长度为…...

git切换远程仓库地址

git 更换远程仓库地址三种方法总结 一、前言 由于之前项目管理使用私服的 gitlab &#xff0c;现在换成了Gitea&#xff0c;需要修改远端仓库地址。 二、环境 windows 10git version 2.34.0.windows.1 三、帮助文档 GitHub文档 四、三种修改方法 方法一&#xff1a;不删除远程仓…...

同步与异步:.NET 中的 Task.WaitAll 和 Task.WhenAll

在 C# 中&#xff0c;异步编程通常涉及同时运行多个任务。处理多个任务的两种常见方法是 Task.WaitAll 和 Task.WhenAll。虽然它们看起来很相似&#xff0c;但它们的用途不同&#xff0c;并且用于不同的场景。本文探讨了 Task.WaitAll 和 Task.WhenAll 之间的区别&#xff0c;并…...

在Linux系统实现瑞芯微RK3588部署rknntoolkit2进行模型转换

一、首先要先安装一个虚拟的环境 安装Miniconda包 Miniconda的官网链接:Minidonda官网 下载好放在要操作的linux系统,我用的是远程服务器的linux系统,我放在whl这个文件夹里面,这个文件夹是我自己创建的 运行安装 安装的操作都是yes就可以了 检查是否安装成功,输入下面…...

【人工智能】Transformers之Pipeline(概述):30w+大模型极简应用

​​​​​​​ 目录 一、引言 二、pipeline库 2.1 概述 2.2 使用task实例化pipeline对象 2.2.1 基于task实例化“自动语音识别” 2.2.2 task列表 2.2.3 task默认模型 2.3 使用model实例化pipeline对象 2.3.1 基于model实例化“自动语音识别” 2.3.2 查看model与task…...

Jenkins中Node节点与构建任务

目录 节点在 Jenkins 中的主要作用 1. 分布式构建 分布式处理 负载均衡 2. 提供不同的运行环境 多平台支持 特殊环境需求 3. 提高资源利用率 动态资源管理 云端集成 4. 提供隔离和安全性 任务隔离 权限控制 5. 提高可扩展性 横向扩展 高可用性 Jenkins 主服务…...

Leetcode3200. 三角形的最大高度

Every day a Leetcode 题目来源&#xff1a;3200. 三角形的最大高度 解法1&#xff1a;模拟 枚举第一行是红色还是蓝色&#xff0c;再按题意模拟即可。 代码&#xff1a; /** lc appleetcode.cn id3200 langcpp** [3200] 三角形的最大高度*/// lc codestart class Solutio…...

docker运行nginx挂载前端html页面步骤

1.常用docker命令 1.docker ps -a 查看所有容器 2.docker ps查看存活的容器 3.docker rm 删除容器 4.docker stop 停止容器运行 5.docker logs 容器id 查看容器日志 6.docker images 查看镜像 7.docker rmi 删除镜像 8.docker exec nginx nginx -s reload 重新加载conf文件…...

kafka部署以及常用命令详细总结

1环境准备 1.1ip规划 ip: 192.168.1.200 1.2配置主机名 #设置主机名 hostnamectl set-hostname node11.3配置hosts [rootnode1 ~]# cat >> /etc/hosts << EOF192.168.1.200 node1 EOF2部署 2.1安装包准备 将以下安装包从官网下载到本地 jdk-8u371-linux-x6…...

代码随想录算法训练营第29天|LeetCode 134. 加油站、135. 分发糖果、860.柠檬水找零、406.根据身高重建队列

1. LeetCode 134. 加油站 题目链接&#xff1a;https://leetcode.cn/problems/gas-station/description/ 文章链接&#xff1a;https://programmercarl.com/0134.加油站.html 视频链接&#xff1a;https://www.bilibili.com/video/BV1jA411r7WX 思路&#xff1a; 贪心&#xff…...

代理模式(大话设计模式)C/C++版本

代理模式 C #include <iostream> using namespace std;class Subject // Subject 定义了RealSubject和Proxy的共用接口..这样就在任何使用RealSubject的地方都可以使用Proxy { public:virtual void func(){cout << "Subject" << endl;} };class R…...

本人学习保存-macOS打开Navicat提示「“Navicat Premium”已损坏,无法打开。 你应该将它移到废纸篓。」的解决方法

新安装了macOS Ventura&#xff0c;打开Navicat Premium&#xff0c;发现会提示&#xff1a; “Navicat Premium”已损坏&#xff0c;无法打开。 你应该将它移到废纸篓。 遇到这种情况&#xff0c;千万别直接移到废纸篓&#xff0c;是有办法解决的。在这里记录一下解决方案。 …...

《Cross-Image Pixel Contrasting for Semantic Segmentation》论文解读

期刊&#xff1a;TPAMI 年份&#xff1a;2024 摘要 研究图像语义分割问题。目前的方法主要集中在通过专门设计的上下文聚合模块(如空洞卷积、神经注意力)或结构感知的优化目标(如iou样损失)挖掘"局部"上下文&#xff0c;即单个图像中像素之间的依赖关系。然而&…...

技术周总结 2024.07.08~07.14(算法,Python,Java,Scala,PHP)

文章目录 一、07.13 周六1.0&#xff09;算法题&#xff1a;字符串中的单词反转1.1&#xff09; 问题01:可靠性计算中的MTTR MTTF MTBF 分别指什么&#xff1f;他们之间有什么联系&#xff1f;MTTR (Mean Time to Repair)MTTF (Mean Time to Failure)MTBF (Mean Time Between F…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

剑指offer20_链表中环的入口节点

链表中环的入口节点 给定一个链表&#xff0c;若其中包含环&#xff0c;则输出环的入口节点。 若其中不包含环&#xff0c;则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

《C++ 模板》

目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板&#xff0c;就像一个模具&#xff0c;里面可以将不同类型的材料做成一个形状&#xff0c;其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式&#xff1a;templa…...

iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈

在日常iOS开发过程中&#xff0c;性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期&#xff0c;开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发&#xff0c;但背后往往隐藏着系统资源调度不当…...

作为测试我们应该关注redis哪些方面

1、功能测试 数据结构操作&#xff1a;验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化&#xff1a;测试aof和aof持久化机制&#xff0c;确保数据在开启后正确恢复。 事务&#xff1a;检查事务的原子性和回滚机制。 发布订阅&#xff1a;确保消息正确传递。 2、性…...