TCP/IP网络编程 第二十章:Windows中的线程同步

同步方法的分类及CRITICAL_SECTION同步

用户模式(User mode)和内核模式(Kernal mode)

Windows操作系统的运行方式是“双模式操作”(Dual-mode Operation),这意味着Windows在运行过程中存在如下2种模式。


□用户模式:运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。

□内核模式:操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受限。
内核是操作系统的核心模块,可以简单定义为如下形式。
□用户模式:应用程序的运行模式。
□内核模式:操作系统的运行模式。
实际上,在应用程序运行过程中,Windows操作系统不会一直停留在用户模式,而是在用户模式和内核模式之间切换。例如,各位可以在Windows中创建线程。虽然创建线程的请求是由应用程序的函数调用完成,但实际创建线程的是操作系统。因此,创建线程的过程中无法避免向内核模式的转换。
定义这2种模式主要是为了提高安全性。应用程序的运行时错误会破坏操作系统及各种资源。特别是C/C++可以进行指针运算,很容易发生这类问题。例如,因为错误的指针运算覆盖了操作系统中存有重要数据的内存区域,这很可能引起操作系统崩溃。但实际上各位从未经历过这类事件,因为用户模式会保护与操作系统有关的内存区域。因此,即使遇到错误的指针运算也仅停止应用程序的运行,而不会影响操作系统。

总之,像线程这种伴随着内核对象创建的资源创建过程中,都要默认经历如下模式转换过程:
用户模式→内核模式→用户模式
从用户模式切换到内核模式是为了创建资源,从内核模式再次切换到用户模式是为了执行应用程序的剩余部分。不仅是资源的创建,与内核对象有关的所有事务都在内核模式下进行。模式切换对系统而言其实也是一种负担,频繁的模式切换会影响性能。

用户模式同步

用户模式同步是用户模式下进行的同步,即无需操作系统的帮助而在应用程序级别进行的同步。用户模式同步的最大优点是——速度快。无需切换到内核模式,仅考虑这一点也比经历内核模式切换的其他方法要快。而且使用方法相对简单,因此,适当运用用户模式同步并无坏处。

但因为这种同步方法不会借助操作系统的力量,其功能上存在一定局限性。稍后将介绍属于用户模式同步的、基于“CRITICAL_SECTION”的同步方法。

内核模式同步

前面已介绍过用户模式同步,即使不另作说明,相信各位也能大概说出内核模式同步的特性及优缺点。下面给出内核模式同步的优点。
□比用户模式同步提供的功能更多。
□可以指定超时,防止产生死锁。
因为都是通过操作系统的帮助完成同步的,所以提供更多功能。特别是在内核模式同步中,可以跨越进程进行线程同步。与此同时,由于无法避免用户模式和内核模式之间的切换,所以性能上会受到一定影响
大家此时很可能想到:“因为是基于内核对象的操作,所以可以进行不同进程之间的同步!”

因为内核对象并不属于某一进程,而是操作系统拥有并管理的。

基于 CRITICAL_SECTION的同步

基于CRITICAL_SECTION的同步中将创建并运用“CRITICAL_SECTION对象”,但这并非内核对象。与其他同步对象相同,它是进入临界区的一把“钥匙”。因此,为了进入临界区,需要得到CRITICAL_SECTION对象这把“钥匙”。相反,离开时应上交CRITICAL_SECTION对象(以下简称CS)。下面介绍CS对象的初始化及销毁相关函数。

#include<windows.h>
void InitilizerCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
IpCriticalSection  //Init...函数中传入需要初始化的CRITICAL_SECTION对象的地址值,Del...函数中传入 //需要解除的CRITICAL_SECTION对象的地址值。

上述函数的参数类型LPCRITICAL_SECTION是CRITICAL_SECTION指针类型。另外DeleteCriticalSection并不是销毁CRITICAL_SECTION对象的函数。该函数的作用是销毁CRITICAL_SECTION对象使用过的(CRITICAL SECTION对象相关的)资源。接下来介绍获取
(拥有)及释放CS对象的函数,可以简单理解为获取和释放“钥匙”的函数。

#include<windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
IpCriticalSection   //获取(拥有)和释放的CRITICAL_SECTION对象的地址值。

与Linux部分中介绍过的互斥量类似,相信大部分人仅靠这些函数介绍也能写出示例程序。

内核模式的同步方法

典型的内核模式同步方法有基于事件(Event)、信号量、互斥量等内核对象的同步,下面从
互斥量开始逐一介绍。

基于互斥量(Mutual Exclusion)对象的同步

基于互斥量对象的同步方法与基于CS对象的同步方法类似,因此,互斥量对象同样可以理解为“钥匙”。首先介绍创建互斥量对象的函数。

#include<windows.h>
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName);
//成功时返回创建的互斥量对象句柄,失败时返回NULL。lpMutexAttributes    //传递安全相关的配置信息,使用默认安全设置时可以传递NULL。blnitialOwner        //如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non- //signaled状态;如果为FALSE,则创建出的互斥量对象不属于任何线程,此 //时状态为signaled。IpName               //用于命名互斥量对象。传入NULL创建无名的互斥量对象。     

从上述参数说明中可以看到,如果互斥量对象不属于任何拥有者,则将进入signaled状态。
利用该特点进行同步。另外,互斥量属于内核对象,所以通过如下函数销毁。

#include<windows.h>
BOOL CloseHandle(HANDLE hObject);//成功时返回TRUE,失败时返回FALSE。hObject    //要销毁对象的句柄

上述函数是销毁内核对象的函数,所同样可以销毁即将介绍的信号量及事件。下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过各位熟悉的WaitForSingleObject函数完成的。

#include<windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
//成功时返回TRUE,失败时返回FALSEhMutex   //需要释放的互斥量对象句柄

接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为non-signaled状态,释放时(未拥有时)进入signaled状态。因此,可以使用WaitForSingleObject函数验证互斥量是否已分配。该函数的调用结果有如下2种。
□调用后进入阻塞状态:互压量对象已被其他线程获取,现处于non-signaled状态。
□调用后直接返回:其他线程未占用互斥量对象,现处于signaled状态。

互斥量在WaitForSingleObject函数返回时自动进入non-signaled状态,因为它是第19章介绍过的"auto-reset"模式的内核对象。结果,WaitForSingleObject函数为申请互斥量时调用的函数。因此,基于互斥量的临界区保护代码如下。

WaitForsingleobject(hMutex, INFINITE);
//临界区的开始
//......
//临界区的结束
ReleaseMutex(hMutex);

WaitForSingleObject函数使互斥量进入non-signaled状态,限制访问临界区,所以相当于临界区的门禁系统。相反,ReleaseMutex函数使互斥量重新进入signaled状态,所以相当于临界区的出口。

基于信号量对象的同步

Windows中基于信号量对象的同步也与Linux下的信号量类似,二者都是利用名为“信号量值”的整数值完成同步的,而且该值都不能小于0。当然,Windows的信号量值注册于内核对象。

下面介绍创建信号量对象的函数,当然,其销毁同样是利用CloseHandle函数进行的。

#include <windows.h>
HANDLE Createsemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,LONG lInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
//成功时返回创建的信号量对象的句柄,失败时返回NULL。IpSemaphoreAttributes   //安全配置信息采用默认安全设置时传递NULL。lInitialCount           //指定信号量的初始值,应大于0小于lMaximumCount。IMaximumCount           //信号量的最大值。该值为1时,信号量变为只能表示0和1的二进制信号量。lpName                  //用于命名信号量对象,传递NULL时创建无名的信号量对象。

可以利用“信量值为0时进入non-signaled状态,大于0时进入signaled状态”的特性进行同步。向lInitialCount参数传递0时,创建non-signaled状态的信号量对象。而向IMaximumCount传入3时,信号量最大值为3,因此可以实现3个线程同时访问临界区时的同步。下面介绍释放信号量对象的函数。

#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG
lpPreviouscount);
//成功时返回TRUE,失败时返回FALSE。hSemaphore     //传递需要释放的信号量对象.IReleaseCount  //释放意味着信号量值的增加,通过该参数可以指定增加的值。超过最大值则不增加,//返回FALSE。IpPreviousCount//用于保存修改之前值的变量地址,不需要时可传递NULL。

信号量对象的值大于0时成为signaled状态,为0时成为non-signaled状态。因此,调用WaitForSingleObject函数时,信号量大于0的情况才会返回。返回的同时将信量值减1,同时进入non-signaled状态(当然,仅限于信号量减1后等于0的情况)。可以通过如下程序结构保护临界区。

WaitForSingleObject(hSemaphore, INFINITE);
//临界区的开始
//..........
//临界区的结束
ReleaseSemaphore(hSemaphore,1, NULL);

基于事件对象的同步

事件同步对象与前2种同步方法相比有很大不同,区别就在于,该方式下创建对象时,在自动以non-signaled状态运行的auto-reset模式和与之相反的manual-reset模式中任选其一。而事件对象的主要特点是可以创建manual-reset模式的对象。首先介绍用于创建事件对象的函数。

#include <windows.h>
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
//成功时返回创建的事件对象句柄,失败时返回NULL。IpEventAttributes     //安全配置相关参塑,采用默认安全配置时传入NULL。bManualReset          //传入TRUE时创建manual-reset模式的事件对象,传入FALSE时创建auto- //reset模式的事件对象。bInitialState         //传入TRUE时创建signaled状态的事件对象,传入FALSE时创建non- //signaled态的事件对象。IpName                //用于命名事件对象。传递NULL时创建无名的事件对象。

相信各位也发现了,上述函数中需要重点关注的是第二个参数。传人TRUE时创建manual-reset模式的事件对象,此时即使WaitForSingleObject函数返回也不会回到non-signaled状态。因此,在这种情况下,需要通过如下2个函数明确更改对象状态。

#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); //设置为non-signaled状态
BOOL SetEvent(HANDLE hEvent);   //设置为signaled状态
//成功时返回TRUE,失败时返回FALSE。

传递事件对象句柄并希望改为non-signaled状态时,应调用ResetEvent函数。如果希望改为signaled状态,则可以调用SetEvent函数。

 Windows平台下实现多线程服务器端

第18章讲完线程的创建和同步方法后,最终实现了多线程聊天服务器端和客户端。按照这种顺序,本章最后也将在Windows平台下实现聊天服务器端和客户端。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<windows.h>
#include<process.h>#define BUF_SIZE 100
#define MAX_CLNT 256unsigned WINAPI HandlerClnt(void *arg);
void SendMsg(char *msg,int len);
void ErrorHandling(char *msg);int clntCnt=0;
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;int main(int argc,char*argv[]){WSADATA wasData;SOCK hServSock,hClntSock;SOCKADDR_IN servAdr,clntAdr;int clntAdrSz;HANDLE hThread;if(argc!=2){printf("Usage : %s <port>\n",argv[0]);exit(1);}if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)ErrorHandling("WSAStartup() error!");hMutex=CreateMutex(NULL,FALSE,NULL);hServSock=socket(PF_INET,SOCK_STREAM,0);memset(&servAdr,0,sizeof(servAdr));servAdr.sin_family=AF_INET;servAdr.sin_addr.s_addr=htonl(INADDR_ANY);servAdr.sin_port=htons(argv[1]);if(bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr))==SOCK_ERROR);Error_Handling("bind() error");if(listen(hServSock,5)==SOCK_ERROR)Error_Handling("listen() error");while(1){clntAdrSz=sizeof(clntAdr);hClntSock=accept(hServSock,(SOCKADDR*)&clntAdr,&clntAdrsz);WaitForSingleObject(hMutex,INFINITE);clntSocks[clntCnt++]=hClntSock;ReleaseMutex(hMutex);hThread=(HANDLE)_beginthreadex(NULL,0,HandleClnt,(void*)&hClntSock,0,NULL);printf("Connected client IP: %s \n",inet_ntoa(clntAdr.sin_addr));}closesocket(hServSock);WSACleanup();return 0;
}unsigned WINAPI HandleClnt(void *arg){SOCKET hClntSock=*((SOCKET*)arg);int setLen=0,i;char msg[BUF_SIZE];while((strLen=recv(hClntSock,msg,sizeof(msg),0))!=0)SendMsg(msg,strLen);WaitForSingleObject(hMutex,INFINITE);for(i=0;i<clntCnt;i++){if(hClntSock==clntSocks[i]){while(i++<clntCnt-1)clntSocks[i]=clntSocks[i+1];break;}}clntCnt--;ReleaseMutex(hMutex);closesocket(hClntSock);return 0;
}void SendMsg(char *msg,int len){//发送给全部人int i;WaitForSingleObject(hMutex,INFINITE);for(i=0;i<clntCnt;++i)send(clntSocks[i],msg,len,0);ReleaseMutex(hMutex);
}void ErrorHangling(char *msg){fputs(msg,stderr);fputc('\n',stderr);exit(1);
}

接下来是聊天客户端。

#include<“头文件声明与之前示例一致,故省略。“>
#define BUF_SIZE 100
#define NAME_SIZE 20unsigned WINAPI SendMsg(void * arg);
unsigned WINAPI RecvMsg(void * arg);
void ErrorHandling(char * msg);char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];int main(int argc, char *argv[]){WSADATA wsaData;SOCKET hSock;SOCKADDR_IN servAdr;HANDLE hSndThread, hRcvThread;if(argc!=4){printf("Usage: %s <IP> <port> <name>\n", argv[0]);exit(1);}if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)ErrorHandling("WSAStartup() error!");sprintf(name,"[%s]", argv[3]);hSock=socket(PF_INET, SOCK_STREAM,0);memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family=AF_INET;servAdr.sin_addr.s_addr=inet_addr(argv[1]);servAdr.sin_port=htons(atoi(argv[2]));if(connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)ErrorHandling("connect() error");hSndThread=(HANDLE)_beginthreadex(NULL,0, Sendmsg,(void*)&hSock,0,NULL);hRcvThread=(HANDLE)_beginthreadex(NULL,0, RecvMsg,(void*)&hSock,0,NULL);WaitForSingleObject(hSndThread,INFINITE);WaitForSingleObject(hRcvThread, INFINITE);closesocket(hSock);WSACleanup();return 0;
}unsigned WINAPI SendMsg(void * arg){SOCKET hSock=*((SOCKET*)arg);char nameMsg[NAME_SIZE+BUF_SIZE];while(1){fgets(msg, BUF_SIZE, stdin);if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")){closesocket(hSock);exit(0);}sprintf(nameMsg,"%s %s",name, msg);send(hSock,nameMsg,strlen(nameMsg),0);}
return 0;
}unsigned WINAPI RecvMsg(void * arg){int hSock=*((SOCKET*)arg);char nameMsg[NAME_SIZE+BUF_SIZE];int strLen;while(1){strLen=recv(hSock,nameMsg, NAME_SIZE+BUF_SIZE-1,0);if(strLen==-1)return -1;nameMsg[strLen]=0;fputs(nameMsg, stdout);}
return 0;
}void ErrorHandling(char *msg){
//与服务器示例的ErrorHandling一致。
}

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

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

相关文章

【分布式学习】服务注册与发现:Eureka、zk、Nacos、Consul对比

服务发现框架对比 –NacosEurekaConsulCoreDNSZookeeper一致性协议CPAPAPCP—CP健康检查TCP/HTTP/MYSQL/Client BeatClient BeatTCP/HTTP/gRPC/Cmd—Keep Alive负载均衡策略权重/metadata/SelectorRibbonFabioRoundRobin—雪崩保护有有无无无自动注销实例支持支持不支持不支持…

预处理过程(2/13)

头文件包含&#xff1a;#include定义一个宏&#xff1a;#define条件编译&#xff1a;#if、#else、#endif编译控制&#xff1a;#pragma 编译器提供的这些预处理命令&#xff0c;大大方便了程序的编写&#xff1a;通过头文件包含可以实现模块化编程&#xff1b;使用宏可以定义一…

关于正则表达式的简单介绍以及使用

一、介绍 正则表达式通常被用来检索匹配某种模式&#xff08;规律&#xff09;的文本 日常文本检索&#xff0c;如果单纯检索某个数字&#xff0c;字母&#xff0c;或者单词匹配出来的结果较多&#xff0c;而面对目标文件内容较大的时&#xff0c;我们也不可能肉眼对检索出来的…

JavaCV error AAC with no global headers is currently not supported

当我使用JavaCV库&#xff08;FFmpegFrameGrabber FFmpegFrameRecorde&#xff09;尝试将dhav码流转为rtsp的时候&#xff0c;出现了以下报错&#xff1a; Error: [rtsp 0000002318df7c30] AAC with no global headers is currently not supported.Exception in thread &quo…

leetcode 542. 01 矩阵

给定一个由 0 和 1 组成的矩阵 mat &#xff0c;请输出一个大小相同的矩阵&#xff0c;其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。 两个相邻元素间的距离为 1 。 示例 1&#xff1a; 输入&#xff1a;mat [[0,0,0],[0,1,0],[0,0,0]] 输出&#xff1a;[[0,0,0],…

docker安装yapi

一&#xff1a;创建docker-compose.yml 创建docker-compose.yml文件&#xff0c;具体内容如下&#xff1a; version: 3services:yapi-web:image: jayfong/yapi #拉取镜像container_name: yapi-web #容器名称ports: - 3000:3000 #端口映射environment:- YAPI_ADMIN_ACCOUN…

解锁新技能《logback packagingData属性配置作用及源码分析》

开源SDK: <dependency><groupId>io.github.mingyang66</groupId><artifactId>oceansky-logger</artifactId><version>4.3.6</version> </dependency> <!-- 基于logback的日志组件SDK --> <dependency><grou…

Hadoop——Hadoop单机搭建问题汇总

1、org.apache.hadoop.security.AccessControlException: Permission denied: userroot... 解决方法&#xff1a;关闭Hadoop,在hdfs-site.xml文件中添加&#xff1a; <property><name>dfs.permissions</name><value>false</value> </proper…

【Unity2D】设置一物体默认在其他物体之上不被遮挡

比如我想让机器人显示在箱子的前面。 点击箱子&#xff0c;将其层级设置在机器人的后面。 即修改箱子的Order in Layer 在机器人之后 物体默认的Order in Layer 都是0 &#xff0c;将箱子的Order in Layer修改为-1即可 这样将确保先绘制机器人&#xff0c;然后绘制箱子。这样…

C#鼠标拖拽,移动图片实例

最近工作需要做一个鼠标可以拖拽移动图片的功能。 写了几个基本功能&#xff0c;勉强能用。这里记录一下。欢迎大神补充。 这个就是完成的功能。 下边的绿色是一个pictureBox&#xff0c;白色框也是一个pictureBox&#xff0c;他们二者是子父级关系。 绿色是父级&#xff0c…

【图论】最短路算法

1、Dijkstra算法 不能处理边权为负的情况&#xff0c;复杂度O(nlogn) 步骤与基本思路 &#xff08;1&#xff09;初始化距离数组dist[N]&#xff0c;将其所有值赋为0x3f&#xff0c;并将起点1的dist初始化为0&#xff0c;存入优先队列heap中 &#xff08;2&#xff09;从所…

shell编程之正则表达式

正则表达式&#xff1a;由一类特殊的字符以及文本字符所编写的一种模式&#xff0c;处理文本当中的内容 其中的一些字符不表示字符的字面含义&#xff0c;表示控制或者通配的功能 通配符&#xff1a;匹配文件名和目录名&#xff0c;不能匹配文件的内容 正则表达式&#xff1a;可…

STM32 BOOTLOADER配置以及APP跳转实现(裸机)

配置实现环境:KEIL 一、STM32BootLoader配置 Bootloader: Bootloader是硬件启动的引导程序,是运行操作系统的前提。在操作系统内核或用户应用程序运行之前运行的一段小代码。对硬件进行相应的初始化和设定,最终为操作系统准备好环境。 APP:APP就是我们的应用程序,经过硬件…

NAS私有云存储 - 搭建Nextcloud私有云盘并公网远程访问

文章目录 摘要视频教程1. 环境搭建2. 测试局域网访问3. 内网穿透3.1 ubuntu本地安装cpolar3.2 创建隧道3.3 测试公网访问 4 配置固定http公网地址4.1 保留一个二级子域名4.1 配置固定二级子域名4.3 测试访问公网固定二级子域名 摘要 Nextcloud,它是ownCloud的一个分支,是一个文…

Centos Certbot 使用

安装 可选配置&#xff1a;启动EPEL存储库 非必要项 yum install -y epel-release yum clean all yum makecache #启用可选通道 可以不配置 yum -y install yum-utils yum-config-manager --enable rhui-REGION-rhel-server-extras rhui-REGION-rhel-server-optional必要配置…

【《Spring Boot微服务实战(第2版)》——一本关于如何在Spring Boot中构建微服务的全面指南】

使用Spring Boot框架构建基于Java的微服务架构&#xff0c;将应用程序从小型单体架构蜕变为由多个服务组成的事件驱动架构。这个最新版本围绕服务发现、负载均衡、路由、集中式日志、按环境配置和容器化等知识点&#xff0c;循序渐进地讲述微服务架构、测试驱动的开发和分布式系…

Mysql 主从复制、读写分离

目录 一、前言&#xff1a; 二、主从复制原理 2.1 MySQL的复制类型 2.2 MySQL主从复制的工作过程; 2.2.1 MySQL主从复制延迟 2.3 MySQL 有几种同步方式&#xff1a; 三种 2.3.1、异步复制&#xff08;Async Replication&#xff09; 2.3.2、同步复制&#xff08;Sync Re…

centos逻辑分区磁盘扩展

最近碰到服务器磁盘空间不足&#xff0c;需要扩展逻辑分区的需求&#xff0c;特地做下小笔记&#xff0c;方便后续自己回忆。下图是磁盘的相关概念示意图&#xff1a; 1、查看磁盘空间 [rootlocalhost ~]# df -h #查看磁盘空间&#xff0c;根分区的大小是18G&#xff0c;已经用…

力扣 -- 918. 环形子数组的最大和

一、题目&#xff1a; 题目链接&#xff1a;918. 环形子数组的最大和 - 力扣&#xff08;LeetCode&#xff09; 二、解题步骤&#xff1a; 下面是用动态规划的思想解决这道题的过程&#xff0c;相信各位小伙伴都能看懂并且掌握这道经典的动规题目滴。 三、参考代码&#xff1…

PCL点云处理之最小二乘直线拟合(2D| 方法2)(❤亲测可用❤)(二百零一)

PCL点云处理之最小二乘直线拟合(2D| 方法2)(❤亲测可用❤)(二百零一) 一、算法简介二、算法实现1.代码2.结果一、算法简介 在二百章中,我们介绍了一种最小二乘拟合直线点云(2D)的方法,可以获取直线方程系数k,b,这里介绍另一种拟合直线点云的方法,更为简单方便,结果…