Linux——多线程(五)

1.线程池

1.1初期框架

thread.hpp

#include<iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{using func_t = std::function<void()>;class Thread{public:void Excute(){_func();}public:Thread(func_t func, std::string name="none-name"): _func(func), _threadname(name), _stop(true){}static void *threadroutine(void *args) //注意:类成员函数,形参是有this指针的{Thread *self = static_cast<Thread *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;//线程tidstd::string _threadname;//线程名字func_t _func;//线程所要执行的函数bool _stop;//判断线程是否停止};
}

 ThreadPool.hpp 

#include<vector>
#include<unistd.h>
#include<string>
#include<queue>
#include"Thread.hpp"using namespace ThreadModule;
const int g_thread_num = 3;//默认线程数
// 线程池->一批线程,一批任务,有任务push、有任务pop,本质是: 生产消费模型
template <typename T>
class ThreadPool
{
public:ThreadPool(int threadnum=g_thread_num)//构造函数:_threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex,nullptr);//初始化锁pthread_cond_init(&_cond,nullptr);//初始化条件变量}void Print(){while(true){std::cout<<"我是一个线程"<<std::endl;sleep(1);}}void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){std::string name = "thread-" + std::to_string(num + 1);_threads.emplace_back(Print,name);//线程处理函数是Print,注意这里有问题}_isrunning = true;}void Start()//启动线程池{for(auto &thread:_threads){thread.Start();std::cout<<thread.name()<<"线程:启动成功"<<std::endl;}}void Wait(){for(auto &thread:_threads){thread.Join();}}// bool Enqueue(const T &t)// {// }~ThreadPool()//析构{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _threadnum;//线程的数量std::vector<Thread> _threads;//用vector来存线程std::queue<T> _task_queue;//任务队列pthread_mutex_t _mutex;//锁pthread_cond_t _cond;//条件变量int _waitnum;//有几个线程阻塞bool _isrunning;//判断线程池是否在运行
};

main.cc

#include <iostream>	
#include <string>
#include <memory>
#include "threadpool.hpp"	int main()
{std::unique_ptr<ThreadPool<int>> tp(new ThreadPool<int>()); tp->InitThreadPool();tp->Start();sleep(5);tp->Wait();return 0;
}

 

 此时会报错:无效使用非静态成员函数...

主要原因是成员函数包含this指针而thread.hpp中线程所要执行函数的参数为空:using func_t = std::function<void()>;,导致参数类型不匹配

有两种解决方法

 方法一:在Print函数前面加上static

    static void Print(){while(true){std::cout<<"我是一个线程"<<std::endl;sleep(1);}}

 

方法二:在初始化线程池时用bind绑定ThreadPool内部的Print方法,缺省地设置参数this,就是将this参数默认的绑定到Print方法上,这样一来就和thread.hpp中的参数匹配上了 

    void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){std::string name = "thread-" + std::to_string(num + 1);//_threads.emplace_back(Print,name);//线程处理函数是Print_threads.emplace_back(std::bind(&ThreadPool::Print,this),name);}_isrunning = true;}

  也是成功运行

就算后面我们需要更改线程的参数
 那么也可以在初始化函数那里固定式的绑定参数了

不需要再去单独给线程设计参数对象了 

一个类的成员方法设计成另一个类的回调方法,常见的实现就是这种

类的成员方法也可以成为另一个类的回调方法,方便我们继续类级别的互相调用

 

1.2代码完善

 

接下来就是如何入队列以及我们的新线程应该做什么任务...

处理任务:每一个线程进来的时候都需要去任务队列中获取任务,所以我们首当其冲的就要对任务队列给它锁住

任务队列的加锁、解锁以及线程的等待与唤醒(条件变量)

private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}

 处理任务

    void HandlerTask(std::string name)//线程处理任务{while (true){//加锁LockQueue();//任务队列中不一定有数据,如果任务队列为空且线程池在跑,那么就阻塞住while(_task_queue.empty()&&_isrunning){_waitnum++;ThreadSleep();_waitnum--;}//如果任务队列是空的,然后线程池又退出了,那么就没必要运行了if(_task_queue.empty() && !_isrunning){UnlockQueue();std::cout<<name<<"quit..."<<std::endl;sleep(1);break;}//不论线程池有没有退出,走到这说明一定有任务 ->处理任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();//解锁t();}}

 注意:这个任务是属于线程独占的任务,不能再任务队列的加锁、解锁之间处理

 入任务队列

如果线程阻塞等待的数量大于0,就唤醒一个线程

 

    bool Enqueue(const T &t){bool ret = false;LockQueue();if(_isrunning){_task_queue.push(t);if(_waitnum>0){ThreadWakeup();}ret = true;}UnlockQueue();return ret;}

threadpool.hpp

任务还没写,所以t()先注释掉

#include<iostream>
#include<vector>
#include<unistd.h>
#include<string>
#include<queue>
#include"LockGuard.hpp"
#include"Thread.hpp"using namespace ThreadModule;
const int g_thread_num = 3;//默认线程数
// 线程池->一批线程,一批任务,有任务push、有任务pop,本质是: 生产消费模型
template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}
public:ThreadPool(int threadnum=g_thread_num)//构造函数:_threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex,nullptr);//初始化锁pthread_cond_init(&_cond,nullptr);//初始化条件变量}// static void Print()// {//     while(true)//     {//         std::cout<<"我是一个线程"<<std::endl;//         sleep(1);//     }// }// void Print(std::string name)// {//     while(true)//     {//         std::cout<<"我是一个线程,线程名是"<<name<<std::endl;//         sleep(1);//     }// }void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){std::string name = "thread-" + std::to_string(num + 1);//_threads.emplace_back(Print,name);//线程处理函数是Print//_threads.emplace_back(std::bind(&ThreadPool::Print,this,std::placeholders::_1),name);_threads.emplace_back(std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1),name);}_isrunning = true;}void Start()//启动线程池{for(auto &thread:_threads){thread.Start();std::cout<<thread.name()<<"线程:启动成功"<<std::endl;}}void HandlerTask(std::string name)//线程处理任务{while (true){//加锁LockQueue();//任务队列中不一定有数据,如果任务队列为空且线程池在跑,那么就阻塞住while(_task_queue.empty()&&_isrunning){_waitnum++;std::cout<<name<<"线程阻塞中..."<<std::endl;ThreadSleep();_waitnum--;}//如果任务队列是空的,然后线程池又退出了,那么就没必要运行了if(_task_queue.empty() && !_isrunning){UnlockQueue();std::cout<<name<<"quit..."<<std::endl;sleep(1);break;}//不论线程池有没有退出,走到这说明一定有任务 ->处理任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();//解锁//t();}}void Stop(){LockQueue();_isrunning = false;ThreadWakeupAll();UnlockQueue();        }void Wait(){for(auto &thread:_threads){thread.Join();}}bool Enqueue(const T &t){bool ret = false;LockQueue();if(_isrunning){_task_queue.push(t);if(_waitnum>0){ThreadWakeup();}ret = true;}UnlockQueue();return ret;}~ThreadPool()//析构{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _threadnum;//线程的数量std::vector<Thread> _threads;//用vector来存线程std::queue<T> _task_queue;//任务队列pthread_mutex_t _mutex;//锁pthread_cond_t _cond;//条件变量int _waitnum;bool _isrunning;//判断线程池是否在运行
};

 main.cc

#include <iostream>	
#include <string>
#include <memory>
#include "Task.hpp"
#include "threadpool.hpp"	int main()
{std::unique_ptr<ThreadPool<int>> tp(new ThreadPool<int>()); tp->InitThreadPool();tp->Start();sleep(2);tp->Stop();tp->Wait();return 0;
}

 

2.加上日志与任务

 LOG.hpp(日志)

#pragma once
#include <iostream>
#include <fstream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include"LockGuard.hpp"
bool gIsSave = false;
const std::string logname = "log.txt";// 1. 日志是有等级的
enum Level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};
void SaveFile(const std::string &filename, const std::string &message)
{std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message;out.close();
}
std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}std::string GetTimeString()
{time_t curr_time = time(nullptr);struct tm *format_time = localtime(&curr_time);if (format_time == nullptr)return "None";char time_buffer[1024];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",format_time->tm_year + 1900,format_time->tm_mon + 1,format_time->tm_mday,format_time->tm_hour,format_time->tm_min,format_time->tm_sec);return time_buffer;
}pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 日志是有格式的
// 日志等级 时间 代码所在的文件名/行数 日志的内容
void LogMessage(std::string filename, int line,bool issave,int level, const char *format, ...)
{std::string levelstr = LevelToString(level);std::string timestr = GetTimeString();pid_t selfid = getpid();char buffer[1024];va_list arg;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer + "\n";LockGuard lockguard(&lock);if (!issave){std::cout << message;}else{SaveFile(logname, message);}
}
#define LOG(level, format, ...)                                                \do                                                                         \{                                                                          \LogMessage(__FILE__, __LINE__, gIsSave, level, format, ##__VA_ARGS__); \} while (0)

 LockGuard.hpp

#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__#include <iostream>
#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};#endif

threadpool.hpp

#include<iostream>
#include<vector>
#include<unistd.h>
#include<string>
#include<queue>	
#include"LOG.hpp"
#include"LockGuard.hpp"
#include"Thread.hpp"using namespace ThreadModule;
const int g_thread_num = 3;//默认线程数
// 线程池->一批线程,一批任务,有任务push、有任务pop,本质是: 生产消费模型
template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadWakeupAll(){pthread_cond_broadcast(&_cond);}
public:ThreadPool(int threadnum=g_thread_num)//构造函数:_threadnum(threadnum), _waitnum(0), _isrunning(false){pthread_mutex_init(&_mutex,nullptr);//初始化锁pthread_cond_init(&_cond,nullptr);//初始化条件变量LOG(INFO, "线程池构造成功");}// static void Print()// {//     while(true)//     {//         std::cout<<"我是一个线程"<<std::endl;//         sleep(1);//     }// }// void Print(std::string name)// {//     while(true)//     {//         std::cout<<"我是一个线程,线程名是"<<name<<std::endl;//         sleep(1);//     }// }void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){std::string name = "thread-" + std::to_string(num + 1);//_threads.emplace_back(Print,name);//线程处理函数是Print//_threads.emplace_back(std::bind(&ThreadPool::Print,this,std::placeholders::_1),name);_threads.emplace_back(std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1),name);LOG(INFO, "线程 %s 初始化成功", name.c_str());}_isrunning = true;}void Start()//启动线程池{for(auto &thread:_threads){thread.Start();std::cout<<thread.name()<<"线程:启动成功"<<std::endl;}}void HandlerTask(std::string name)//线程处理任务{LOG(INFO, "%s 正在运行...", name.c_str());while (true){//加锁LockQueue();//任务队列中不一定有数据,如果任务队列为空且线程池在跑,那么就阻塞住while(_task_queue.empty()&&_isrunning){_waitnum++;ThreadSleep();_waitnum--;}//如果任务队列是空的,然后线程池又退出了,那么就没必要运行了if(_task_queue.empty() && !_isrunning){UnlockQueue();//std::cout<<name<<"quit..."<<std::endl;sleep(1);break;}//不论线程池有没有退出,走到这说明一定有任务 ->处理任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();//解锁LOG(DEBUG, "%s 获得任务", name.c_str());t();LOG(DEBUG,"%s 处理任务中,结果是%s",name.c_str(), t.ResultToString().c_str());}}void Stop(){LockQueue();_isrunning = false;ThreadWakeupAll();UnlockQueue();        }void Wait(){for(auto &thread:_threads){thread.Join();LOG(INFO, "%s 线程退出...", thread.name().c_str());}}bool Enqueue(const T &t){bool ret = false;LockQueue();if(_isrunning){_task_queue.push(t);if(_waitnum>0){ThreadWakeup();}LOG(DEBUG, "任务入队列成功");ret = true;}UnlockQueue();return ret;}~ThreadPool()//析构{pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
private:int _threadnum;//线程的数量std::vector<Thread> _threads;//用vector来存线程std::queue<T> _task_queue;//任务队列pthread_mutex_t _mutex;//锁pthread_cond_t _cond;//条件变量int _waitnum;bool _isrunning;//判断线程池是否在运行
};

thread.hpp

#include<iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{using func_t = std::function<void(std::string)>;class Thread{public:void Excute(){_func(_threadname);}public:Thread(func_t func, std::string name="none-name"): _func(func), _threadname(name), _stop(true){}static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!{Thread *self = static_cast<Thread *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;//线程tidstd::string _threadname;//线程名字func_t _func;//线程所要执行的函数bool _stop;//判断线程是否停止};
}

 

 main.cc

#include <iostream>	
#include <string>
#include <memory>
#include "LOG.hpp"
#include "threadpool.hpp"	
#include "Task.hpp"	
#include<ctime>int main()
{srand(time(nullptr) ^ getpid() ^ pthread_self());std::unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>(5)); tp->InitThreadPool();tp->Start();int tasknum=3;while(tasknum){int a = rand() % 12 + 1;usleep(1000);int b = rand() % 4 + 1;Task t(a, b);LOG(INFO, "主线程推送任务: %s", t.DebugToString().c_str());tp->Enqueue(t);sleep(1);tasknum--;}tp->Stop();tp->Wait();return 0;
}

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

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

相关文章

Redis 7.x 系列【21】主从复制

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2. 工作原理2.1 建立连接2.2 全量复制2.3 命令传播2.4 增量复制 3. 拓扑架构3.…

Uniapp表单提交

template中&#xff1a; <template><view class""><button class"tianjia" click"tianjia">添加</button><view class"divOne" v-show"a"><text class"guanbi" click"gua…

本地 HTTP 文件服务器的简单搭建 (deno/std)

首发日期 2024-06-30, 以下为原文内容: 在本地局域网搭建一个文件服务器, 有很多种方式. 本文介绍的是窝觉得比较简单的一种. 文件直接存储在 btrfs 文件系统之中, 底层使用 LVM 管理磁盘, 方便扩容. 使用 btrfs RAID 1 进行镜像备份 (一个文件在 2 块硬盘分别存储一份), 防止…

网络通信、BIO、NIO

1. 涉及的网络基础知识 Socket&#xff1a; 操作系统提供的api&#xff0c;介于应用层和tcp/ip层之间的软件层&#xff0c;封装服务器客户端之间网络通信相关内容&#xff0c;方便调用 IO多路复用&#xff1a; &#xff08;I/O Multiplexing&#xff09;是一种IO操作模式&a…

Python 的 metaclass

文章目录 先说结论1. metaclass 的作用2. 主要的执行过程 1. metaclass.__new__2. metaclass.__call__关于 metaclass.__init__ 3. metaclass.__prepare__4. 自动创建 __slots__ 属性4.1 metaclass 的接口类4.2 metaclass conflict 5. Class metaprogramming 先说结论 1. meta…

Java技术栈总结:JVM虚拟机篇

一、Java的四种引用类型 1、强引用 最常见的引用&#xff0c;类似Object obj new Object()、String str “hello”。如果一个对象具有强引用&#xff0c;垃圾回收器绝对不会回收它。即使抛出“OutOfMemoryError”错误&#xff0c;程序终止&#xff0c;也不会随意回收具有强引…

20240710 每日AI必读资讯

&#x1f916;微软&#xff1a;不会像 OpenAI 一样阻止中国访问 AI 模型 - OpenAI 将于周二&#xff08;7 月 9 日&#xff09;开始阻止中国用户访问其 API。 - 微软发言人表示&#xff1a;Azure OpenAI API服务在中国的提供方式没有变化。 - 公司仍然通过部署在中国以外地区…

妙笔生词智能写歌词软件:创新助力还是艺术之殇?

在音乐创作日益普及和多样化的当下&#xff0c;各种辅助工具层出不穷&#xff0c;妙笔生词智能写歌词软件便是其中之一。那么&#xff0c;它到底表现如何呢&#xff1f; 妙笔生词智能写歌词软件&#xff08;veve522&#xff09;的突出优点在于其便捷性和高效性。对于那些灵感稍…

c/c++:牛客小白月赛93

比赛链接 A 生不逢七 题目描述(题目链接添加链接描述)&#xff1a; 睡前游戏中最简单又最好玩的游戏就是这个啦&#xff01; 该游戏规则为&#xff1a;多名玩家轮流报数&#xff0c;当要报的数字中含有 7 或者是 7 的倍数时&#xff08;例如 37&#xff0c;49&#xff09;&…

腾讯又一平台即将停止运营

随着腾讯公司业务和战略的调整&#xff0c;某些业务逐渐退出历史舞台&#xff0c;如“腾讯直播平台NOW”&#xff0c;以及“QQ签到”&#xff0c;“腾讯待办”&#xff0c;“企鹅FM音频平台”等&#xff0c;最近又有一则重磅消息&#xff0c;那就是“腾讯课堂”也即将停止运营。…

类似评论、省市区这种具有层次结构的数据表怎么设计?

业务功能模块 评论、回复模块省市区表 设置一个给每个数据设置一个parent_id 例如&#xff1a; 某个视频下a写了条评论&#xff0c;那a的parent_id就是0;b回复了a&#xff0c;那b的parent_id就是a的id;c回复了b&#xff0c;那c的parent_id就是b的id; 这样&#xff0c;所有评论…

Mosh|初学者 SQL 教程

sql文件链接&#xff1a;链接: https://pan.baidu.com/s/1okjsgssdxMkfKf8FEos7DA?pwdf9a9 提取码: f9a9 在mysql workbench 导入 create_databases.sql 文件&#xff0c;下面是运行成功的界面 快捷方式&#xff1a;全部运行可以同时按下controlcommandenter &#xff0c;或者…

ceph存储

1 存储简介 存储的三种方式包括&#xff1a;块存储、文件存储、对象存储1。此外&#xff0c;还有内存存储、硬盘存储和闪存存储2。 内存存储&#xff1a;临时性数据存储方式&#xff0c;存储速度快&#xff0c;容量有限&#xff0c;通常用来存储正在使用的程序和数据。硬盘存…

【通信协议】八、CDL(Caterpillar Data Link)协议解析

1、协议简介 CDL(Caterpillar Data Link)是caterpillar的通信协议&#xff0c;该品牌发动机ECM与各控制单元进行通信时&#xff0c;采用基于RS-485的物理层规范进行开发的CDL协议进行通信&#xff1b; 2、物理层 信号传输方式&#xff1a;差分信号&#xff08;通过两条线的电…

稀疏建模介绍,详解机器学习知识

目录 一、什么是机器学习&#xff1f;二、稀疏建模介绍三、Lasso回归简介四、Lasso超参数调整与模型选择 一、什么是机器学习&#xff1f; 机器学习是一种人工智能技术&#xff0c;它使计算机系统能够从数据中学习并做出预测或决策&#xff0c;而无需明确编程。它涉及到使用算…

集训 Day 2 模拟赛总结

复盘 7&#xff1a;30 开题 想到几天前被普及组难度模拟赛支配的恐惧&#xff0c;下意识觉得题目很难 先看 T1&#xff0c;好像不是很难&#xff0c;魔改 Kruskal 应该就行 看 T2 &#xff0c;感觉很神奇&#xff0c;看到多串匹配想到 AC 自动机&#xff0c;又想了想 NOIP …

328. 奇偶链表

https://leetcode.cn/problems/odd-even-linked-list/https://leetcode.cn/problems/odd-even-linked-list/ 解题思路&#xff1a; 把第一个和第二个节点分别作为奇数、偶数的头节点&#xff0c;当遇到奇节点&#xff0c;删除&#xff0c;并插入到奇数头节点后&#xff0c;这样…

PPI(每英寸像素数)、DPI(每英寸点数)和Pixel(像素)的区别和联系?

一、定义 PPI、DPI和Pixel是图像处理、打印和显示领域中常用的三个概念&#xff0c;它们之间既有区别又有联系。以下是对这三个概念进行分别讲解&#xff1a; 1. PPI&#xff08;Pixels Per Inch&#xff09;&#xff0d;即每英寸像素数&#xff0c;是图像分辨率的一种表示方…

理解点对点协议:构建高效网络通信

在通信线路质量较差的年代&#xff0c;能够实现可靠传输的高级数据链路控制&#xff08;High-level Data Link Control, HDLC&#xff09;协议曾是比较流行的数据链路层协议。HDLC是一个较复杂的协议&#xff0c;实现了滑动窗口协议&#xff0c;并支持点对点和点对多点两种连接…

钉钉扫码登录第三方

钉钉文档 实现登录第三方网站 - 钉钉开放平台 (dingtalk.com) html页面 将html放在 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>登录</title>// jquery<script src"http://code.jqu…