线程池:线程池的实现 | 日志

在这里插入图片描述

🌈个人主页: 南桥几晴秋
🌈C++专栏: 南桥谈C++
🌈C语言专栏: C语言学习系列
🌈Linux学习专栏: 南桥谈Linux
🌈数据结构学习专栏: 数据结构杂谈
🌈数据库学习专栏: 南桥谈MySQL
🌈Qt学习专栏: 南桥谈Qt
🌈菜鸡代码练习: 练习随想记录
🌈git学习: 南桥谈Git

🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈
本科在读菜鸡一枚,指出问题及时改正

文章目录

  • 原理
  • 线程池实现
  • 日志
    • 获取当前时间函数接口
    • 启用类型设置
    • 输出到屏幕
    • 输出到文件
    • 选择输出方式
    • 创建日志消息
    • 完整代码
  • 携带日志的线程池设计

原理

在一个可执行程序内部存在多个线程和一个任务队列。如果任务队列里长时间没有任务,这些线程就会休眠,如果此时来了一个任务,那么线程就会被唤醒。像这种,提前创建好线程,需要的时候直接使用,我们称之为线程池。这种本质上就是一个生产消费模型。
在这里插入图片描述

线程池实现

//ThreadPool.hpp
#pragma once#include<iostream>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>
#include<functional>
#include"Thread.hpp"using namespace threadModel;static const int gdefaultnum=5;void test()
{while(true){std::cout<<"hello world"<<std::endl;sleep(1);}
}template<typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string& name)  // this{while (true){LockQueue();//如果队列为空(有任务)while(IsEmpty()&&_isrunning) //线程没有任务,但是在工作,继续休眠{_sleep_thread_num++;Sleep();_sleep_thread_num--;}if(IsEmpty()&&!_isrunning) // 任务是空的,并且线程退出工作{std::cout<<name<<" quit..."<<std::endl;UnlockQueue();break;}// 队列不为空,有任务 或者 队列被唤醒// 取任务T t=_task_queue.front();_task_queue.pop();UnlockQueue();// 此处任务已经不在任务队列中,任务已经被拿走,处理任务和临界资源是两码事t(); // 处理任务,不能不用也不能在临界区中处理std::cout<<name<<": "<<t.result()<<std::endl;}}public:ThreadPool(int thread_num=gdefaultnum):_thread_num(thread_num),_isrunning(false) //刚开始线程没有使用,_sleep_thread_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);}void Init(){func_t func=std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1);for(int i=0;i<_thread_num;i++){std::string threadname="thread-"+std::to_string(i+1);_threads.emplace_back(threadname,func);}}void Start(){_isrunning=true;for(auto& thread:_threads){thread.Start();}}void Stop(){LockQueue();_isrunning=false;WakeupAll();UnlockQueue();}void Equeue(const T &in){LockQueue();if(_isrunning){_task_queue.push(in);// 如果当前有线程在等待,需要唤醒if(_sleep_thread_num>0){Wakeup();}}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _thread_num;std::vector<Thread> _threads;  // 管理多个线程std::queue<T> _task_queue; // 任务队列bool _isrunning; //当前线程是否在工作int _sleep_thread_num;   //计数器:休眠的线程个数pthread_mutex_t _mutex;pthread_cond_t _cond;
};

在这里插入图片描述

日志

日志是软件运行的记录信息,可以向显示器打印,也可以向文件中打印,日志必须有特定的格式:

  • [日志等级] [pid] [filename] [filenumber] [time] 日志内容(支持可变参数)

日志等级:DEBUG、INFO、WARNING、ERROR、FATAL(致命的)

// 日志等级
enum
{DEBUG=1,INFO,WARING,ERROR,FATAL
};
  • 日志消息:日志等级、id、文件名、行号、当前时间等
// 日志消息
class logmessage
{public:std::string _level; // 日志等级pid_t _id; std::string _filename; // 文件名int _filenumber;  // 行号std::string _cur_time;std::string _message_info;};

获取当前时间函数接口

std::string GetCurTime()
{time_t now=time(nullptr);struct tm* cur_time=localtime(&now);char buffer[128];snprintf(buffer,sizeof(buffer),"%d-%02d-%02d %02d:%02d:%02d",cur_time->tm_year+1900,cur_time->tm_mon+1,cur_time->tm_mday,cur_time->tm_hour,cur_time->tm_min,cur_time->tm_sec);return std::string(buffer);
}
  • time(nullptr) 返回当前的时间(从 1970 年 1 月 1 日到现在的秒数),并将其赋值给 now 变量。time_t 是表示时间点的类型。
  • localtime(&now) now 转换为当地时间,并返回一个指向 tm 结构的指针。tm 结构包含了年、月、日、时、分、秒等信息。
    在这里插入图片描述

启用类型设置

void Enable(int type)
{_type=type;
}

Enable 函数用于设置日志输出类型,可以选择输出到屏幕或文件。

输出到屏幕

void FlushLogToSCreen(const logmessage& lg)
{printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());
}

FlushLogToSCreen 函数将日志信息格式化并输出到控制台。使用 printf 格式化字符串。

输出到文件

void FlushLogToFile(const logmessage& lg)
{std::ofstream out(_logfile,std::ios::app); // 追加打印if(!out.is_open())return;char logtxt[2048];snprintf(logtxt,sizeof(logtxt),"[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());out.write(logtxt,strlen(logtxt));out.close();
}

FlushLogToFile 函数将日志信息写入指定的文件。以追加模式打开文件,并在打开失败时返回。
使用snprintf 格式化日志信息,然后将其写入文件。

选择输出方式

void FlushLog(const logmessage& lg)
{switch(_type){case SCREEN_TYPE:FlushLogToSCreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}
}

创建日志消息

void logMessage(std::string filename,int filenumber,int level,const char* format,...)
{logmessage lg;lg._level=LevelToString(level);lg._id=getpid();lg._filename=filename;lg._filenumber=filenumber;lg._cur_time=GetCurTime();va_list ap;va_start(ap,format);char log_info[1024];vsnprintf(log_info,sizeof(log_info),format,ap);va_end(ap);lg._message_info=log_info;FlushLog(lg);
}
  • logMessage 函数用于创建一条新的日志消息。它接受文件名、文件编号、日志级别和格式化字符串作为参数。
  • 使用可变参数处理(va_list)来处理格式化字符串。
  • 将生成的日志信息存储在 lg 对象中,并调用 FlushLog 函数进行输出。
  • va_list ap;声明了一个 va_list 类型的变量 ap,它用于存储可变参数列表。在 C 语言中,va_list 是一个用于遍历不定数量参数的类型。
  • va_start(ap, format);va_start 宏初始化 ap 以指向函数参数列表中的第一个可变参数。这里的 format 是最后一个固定参数,va_start 会从它的下一个参数开始读取可变参数。因此,ap 现在可以用来访问 format 之后的所有参数。
  • va_end(ap)用于清理va_list 变量 ap。在读取完可变参数后,调用 va_end 是良好的实践,它可以释放与 ap 相关的资源(如果有的话)。

完整代码

#pragma once#include<iostream>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<unistd.h>
#include<stdarg.h>
#include<ctime>
#include<fstream>
#include<pthread.h>
#include<cstdio>
#include"LockGuard.hpp"using std::cin;
using std::cout;
using std::endl;namespace log_ns
{// 日志等级enum{DEBUG=1,INFO,WARING,ERROR,FATAL};// 日志消息class logmessage{public:std::string _level; // 日志等级pid_t _id; std::string _filename; // 文件名int _filenumber;  // 行号std::string _cur_time;std::string _message_info;};pthread_mutex_t glock=PTHREAD_MUTEX_INITIALIZER; // 定义一个全局的锁std::string LevelToString(int level){LockGuard lockguard(&glock);switch(level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARING:return "WARING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetCurTime(){time_t now=time(nullptr);struct tm* cur_time=localtime(&now);char buffer[128];snprintf(buffer,sizeof(buffer),"%d-%02d-%02d %02d:%02d:%02d",cur_time->tm_year+1900,cur_time->tm_mon+1,cur_time->tm_mday,cur_time->tm_hour,cur_time->tm_min,cur_time->tm_sec);return std::string(buffer);}#define SCREEN_TYPE 1#define FILE_TYPE 2const std::string glogfile="./log.txt";// 日志class Log{public:Log(const std::string& logfeile=glogfile):_logfile(logfeile),_type(SCREEN_TYPE){}void Enable(int type){_type=type;}void FlushLogToSCreen(const logmessage& lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage& lg){std::ofstream out(_logfile,std::ios::app); // 追加打印if(!out.is_open())return;char logtxt[2048];snprintf(logtxt,sizeof(logtxt),"[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._cur_time.c_str(),lg._message_info.c_str());out.write(logtxt,strlen(logtxt));out.close();}void FlushLog(const logmessage& lg){switch(_type){case SCREEN_TYPE:FlushLogToSCreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename,int filenumber,int level,const char* format,...){logmessage lg;lg._level=LevelToString(level);lg._id=getpid();lg._filename=filename;lg._filenumber=filenumber;lg._cur_time=GetCurTime();va_list ap;va_start(ap,format);char log_info[1024];vsnprintf(log_info,sizeof(log_info),format,ap);va_end(ap);lg._message_info=log_info;//cout<<lg._message_info<<endl;// 打印日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level,Format,...) do{lg.logMessage(__FILE__,__LINE__,Level,Format,##__VA_ARGS__);}while(0)#define EnableScreen() do{lg.Enable(SCREEN_TYPE);}while(0)#define EnableFile() do{lg.Enable(FILE_TYPE);}while(0)};

在这里插入图片描述

携带日志的线程池设计

//ThreadPool.hpp
#pragma once#include<iostream>
#include<unistd.h>
#include<string>
#include<vector>
#include<queue>
#include<functional>
#include"Thread.hpp"
#include"Log.hpp"using namespace threadModel;
using namespace log_ns;static const int gdefaultnum=5;void test()
{while(true){std::cout<<"hello world"<<std::endl;sleep(1);}
}template<typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond,&_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string& name)  // this{while (true){LockQueue();//如果队列为空(有任务)while(IsEmpty()&&_isrunning) //线程没有任务,但是在工作,继续休眠{_sleep_thread_num++;LOG(INFO,"%s thread sleep begin!\n",name.c_str());Sleep();LOG(INFO,"%s thread wakeup!\n",name.c_str());_sleep_thread_num--;}if(IsEmpty()&&!_isrunning) // 任务是空的,并且线程退出工作{UnlockQueue();LOG(INFO,"%s quit\n",name.c_str());break;}// 队列不为空,有任务 或者 队列被唤醒// 取任务T t=_task_queue.front();_task_queue.pop();UnlockQueue();// 此处任务已经不在任务队列中,任务已经被拿走,处理任务和临界资源是两码事t(); // 处理任务,不能不用也不能在临界区中处理LOG(DEBUG,"hander task done, task is: \n%s",t.result().c_str());}}public:ThreadPool(int thread_num=gdefaultnum):_thread_num(thread_num),_isrunning(false) //刚开始线程没有使用,_sleep_thread_num(0){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);}void Init(){func_t func=std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1);for(int i=0;i<_thread_num;i++){std::string threadname="thread-"+std::to_string(i+1);_threads.emplace_back(threadname,func);LOG(DEBUG,"construct thread %s done, init success.\n",threadname.c_str());}}void Start(){_isrunning=true;for(auto& thread:_threads){LOG(DEBUG,"Start thread %s done.\n",thread.Name().c_str());thread.Start();}}void Stop(){LockQueue();_isrunning=false;WakeupAll();UnlockQueue();LOG(INFO,"Thread Pool Stop Success!\n");}void Equeue(const T &in){LockQueue();if(_isrunning){_task_queue.push(in);// 如果当前有线程在等待,需要唤醒if(_sleep_thread_num>0){Wakeup();}}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _thread_num;std::vector<Thread> _threads;  // 管理多个线程std::queue<T> _task_queue; // 任务队列bool _isrunning; //当前线程是否在工作int _sleep_thread_num;   //计数器:休眠的线程个数pthread_mutex_t _mutex;pthread_cond_t _cond;
};
//Main.cc
#include"ThreadPool.hpp"
#include"Task.hpp"
#include"Log.hpp"
#include<memory>using std::cin;
using std::cout;
using std::endl;
using namespace log_ns;int main()
{EnableScreen();//std::unique_ptr<ThreadPool> tp=std::make_unique<>();  //构建一个ThreadPool对象ThreadPool<Task> *tp=new ThreadPool<Task>();tp->Init();tp->Start();int cnt=10;while (cnt){// 不断地向线程池推送任务sleep(1);Task t(1,1);tp->Equeue(t);LOG(INFO,"equeue a task, %s\n",t.debug().c_str());sleep(1);}tp->Stop();LOG(INFO,"thread pool stop!\n");sleep(10);return 0;
}
// Thread.hpp
#pragma once
#include <pthread.h>
#include <iostream>
#include <string>
#include<functional>namespace threadModel
{// 线程执行的方法//typedef void (*func_t)(ThreadData* td);using func_t=std::function<void(const std::string&)>;class Thread{public:void Excute(){_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func) : _name(name), _func(func){}// void *ThreadRoutine(void* args)实际上参数里面还有一个Thread* thisstatic void *ThreadRoutine(void *args) // 加上static后,参数里面就没有Thread* this{Thread *self = static_cast<Thread *>(args); // 获得当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if (n != 0)return false;return true;}std::string Status(){if (_isrunning)return "running";elsereturn "sleep";}void Stop(){if (_isrunning){pthread_cancel(_tid);_isrunning = false;}}void Join(){pthread_join(_tid, nullptr);}std::string Name(){return _name;}~Thread(){Stop();}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程执行的回调函数};
}
//Task.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>class Task
{public:Task(){}Task(int x,int y):_x(x),_y(y){}void Excute(){_result=_x+_y;}void operator()(){Excute();}std::string debug(){std::string msg=std::to_string(_x)+"+"+std::to_string(_y)+"=?";return msg;}std::string result(){std::string msg=std::to_string(_x)+"+"+std::to_string(_y)+"="+std::to_string(_result);return msg;}private:int _x;int _y;int _result;
};

在这里插入图片描述

在这里插入图片描述

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

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

相关文章

【git lfs 问题记录】

报错如下 WARNING: error running /usr/lib/git-core/git ‘config’ ‘–includes’ ‘–global’ ‘–replace-all’ ‘filter.lfs.smudge’ ‘git-lfs smudge – %f’: ‘error: could not write config file /root/.gitconfig: Device or resource busy’ ‘exit status 4…

基于STM32的智能家居交互终端:使用FreeRTOS与MQTT协议的流程设计

一、项目概述 简要介绍项目的目标和用途 随着智能家居的普及&#xff0c;家庭智能交互终端成为提升居住体验的重要设备。本文将介绍一个基于STM32的家庭智能交互终端的设计与实现&#xff0c;该终端能够通过触摸屏、语音识别和传感器数据采集等功能&#xff0c;提供家庭环境监…

数值计算的程序设计问题举例

### 数值计算的程序设计问题 #### 1. 结构静力分析计算 **涉及领域**&#xff1a;工程力学、建筑工程 **主要问题**&#xff1a;线性代数方程组&#xff08;Linear Algebraic Equations&#xff09; **解释说明**&#xff1a; 在结构静力分析中&#xff0c;我们需要解决复杂的…

linux系统解压zip文件名乱码

这是 zip 格式本身的缺陷导致的。zip 格式并没有指定文件名的编码格式&#xff0c;在压缩和解压时均使用操作系统本地编码&#xff0c;Windows 下简体中文为 GBK/GB2312 编码&#xff0c;Linux 下为 UTF-8 编码&#xff0c;两者不一致就造成了乱码。 解决方案&#xff1a; 如…

C++:类中的特殊关键字,运算重载符

1.My_string类中重载以下的运算符&#xff1a; 、[] 、>、<、、>、<、&#xff01;、、输入输出(>>、<<) 主函数&#xff1a; #include <iostream> #include "my_string.h"using namespace std;int main() {My_string s1("cat…

基于SpringBoot+Vue的个人健康管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

【STM32-HAL库】自发电型风速传感器(使用STM32F407ZGT6)(附带工程下载链接)

一、自发电型风速传感器介绍 自发电型风速传感器&#xff0c;也称为风力发电型风速传感器或无源风速传感器&#xff0c;是一种不需要外部电源即可工作的风速测量设备。这种传感器通常利用风力来驱动内部的发电机构&#xff0c;从而产生电能来供电测量风速的传感器部分。以下是自…

GS-SLAM论文阅读笔记--GEVO

前言 这篇文章看着就让人好奇。众所周知&#xff0c;高斯是一个很不错的建图方法&#xff0c;但是本文的题目居然是只用高斯进行单目VO&#xff0c;咱也不知道这是怎么个流程&#xff0c;看了一下作者来自于MIT&#xff0c;说不定是个不错的工作&#xff0c;那就具体看看吧&am…

springboot实战学习(10)(ThreadLoacl优化获取用户详细信息接口)(重写拦截器afterCompletion()方法)

接着学习。之前的博客的进度&#xff1a;完成用户模块的注册接口的开发以及注册时的参数合法性校验、也基本完成用户模块的登录接口的主逻辑的基础上、JWT令牌"的组成与使用、完成了"登录认证"&#xff08;生成与验证JWT令牌&#xff09;以及完成获取用户详细信…

APISIX 联动雷池 WAF 实现 Web 安全防护

Apache APISIX 是一个动态、实时、高性能的云原生 API 网关&#xff0c;提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。 雷池是由长亭科技开发的 WAF 系统&#xff0c;提供对 HTTP 请求的安全请求&#xff0c;提供完整的 API 管理和…

解决 Sqoop 导入 Hive 时时间字段精度丢失问题

目录 一、背景介绍 二、问题描述 三、问题原因 四、解决方案 五、结论 一、背景介绍 介绍 Sqoop 数据导入过程&#xff0c;尤其是从 MySQL 导入 Hive 的场景。说明 MySQL 和 Hive 的数据类型差异&#xff0c;特别是 DATETIME 和 TIMESTAMP 类型的精度问题。 二、问题描述…

MySQL深度分页

在现代Web应用中&#xff0c;数据的逐步展示除了增强用户体验外&#xff0c;还有效提高了系统性能。然而&#xff0c;随着数据集的不断增大&#xff0c;尤其是在数据库表中记录数量达到百万甚至千万级别时&#xff0c;处理深度分页&#xff08;即访问较后页的数据&#xff09;就…

JetLinks物联网平台微服务化系列文章介绍

橙蜂智能公司致力于提供先进的人工智能和物联网解决方案&#xff0c;帮助企业优化运营并实现技术潜能。公司主要服务包括AI数字人、AI翻译、AI知识库、大模型服务等。其核心价值观为创新、客户至上、质量、合作和可持续发展。 橙蜂智农的智慧农业产品涵盖了多方面的功能&#x…

【CKA】二、节点管理-设置节点不可用

2、节点管理-设置节点不可用 1. 考题内容&#xff1a; 2. 答题思路&#xff1a; 先设置节点不可用&#xff0c;然后驱逐节点上的pod 这道题就两条命令&#xff0c;直接背熟就行。 也可以查看帮助 kubectl cordon -h kubectl drain -h 参数详情&#xff1a; –delete-empty…

YOLO11震撼发布!

非常高兴地向大家介绍 Ultralytics YOLO系列的新模型&#xff1a; YOLO11&#xff01; YOLO11 在以往 YOLO 模型基础上带来了一系列强大的功能和优化&#xff0c;使其速度更快、更准确、用途更广泛。主要改进包括 增强了特征提取功能&#xff0c;从而可以更精确地捕捉细节以更…

在树莓派上基于 LNMP 搭建 Nextcloud

原文链接&#xff1a;https://blog.iyatt.com/?p17296 环境 树莓派CM4raspios 20240704 Debian 12 arm64 搭建 LNMP 环境 安装 Nginx sudo apt update sudo apt install -y nginx安装 php 及功能组件支持 参考&#xff1a;https://docs.nextcloud.com/server/latest/adm…

网关的作用及其高可用性设计详解

引言 在现代分布式系统架构中&#xff0c;网关&#xff08;Gateway&#xff09;是一个关键组件。它作为客户端与后端服务之间的桥梁&#xff0c;不仅提供了请求路由、负载均衡、安全认证、流量控制等功能&#xff0c;还能够保护后端服务的安全和稳定性。网关的设计和高可用性对…

EXCEL图片链接快速批量转成图片

EXCEL图片链接快速批量转成图片 直接上图 "<table><img src"&C1&" height50 width50></table>"复制F列到txt文件&#xff0c;暂时放置 全选复制&#xff0c;然后插入一列&#xff0c;粘贴到新的一列中去如图一所示。 ps&…

光通信——PON技术

PON网络结构 PON&#xff08;Passive Optical Network&#xff0c;无源光网络&#xff09;系统的基本组成包括OLT&#xff08;Optical Line Terminal&#xff0c;光线路终端&#xff09;、ODN&#xff08;Optical Distribution Network&#xff0c;光分配单元&#xff09;和ON…

初学51单片机之I2C总线与E2PROM二

总结下上篇博文的结论&#xff1a; 1&#xff1a;ACK信号在SCL为高电平期间会一直保持。 2&#xff1a;在字节数据传输过程中如果发送电平跳变&#xff0c;那么电平信号就会变成重复起始或者结束的信号。&#xff08;上篇博文的测试方法还是不能够明确证明这个结论&#xff0…