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

【多线程中的线程安全问题】线程互斥

1 🍑线程间的互斥相关背景概念🍑

先来看看一些基本概念:

  • 1️⃣临界资源:多线程执行流共享的资源就叫做临界资源。
  • 2️⃣临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • 3️⃣互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

互斥量mutex:

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量(比如全局变量),会带来一些问题。

比如一个大家熟知的栗子:售票。我们用一个全局整形变量记录票的个数,多个线程并发的去抢票,我们不难写出下面这样的代码:

int g_ticket=10000;void* Run(void* args)
{string name=static_cast<const char*>(args);while(true){if(g_ticket<=0){break;}else{cout<<"I am "<<name<<",is running  tickets"<<g_ticket<<endl;g_ticket--;}usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i=0;i<5;++i){char* name=new char[26];snprintf(name,26,"pthread%d",i+1);pthread_create(ptids+i,nullptr,Run,name);}for(int i=0;i<5;++i){pthread_join(ptids[i],nullptr);}return 0;
}

当我们运行时:
在这里插入图片描述
我们发现,有多个线程抢到了同一张票,并且打印混乱。有些情况下票还有可能变成了负数,而这就是线程不安全所带来的问题,解决办法我们在下面会给出详细解释。


2 🍑用互斥锁解决线程安全问题🍑

2.1 🍎分析问题 🍎

我们来分析下上面的代码为什么会出现那样的结果?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • 减减ticket 操作本身就不是一个原子操作。

我们可以取出渐渐ticket取出ticket–部分的汇编代码:

objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

- - 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中;
  • update : 更新寄存器里面的值,执行-1操作;
  • store :将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:

  • 1️⃣代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 2️⃣如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 3️⃣如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

2.2 🍎互斥量的接口 🍎

🍋初始化互斥量🍋

初始互斥量有两种方式:

  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

这两种方式选择哪一种都是OK的。

🍋销毁互斥量🍋

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁;
  • 不要销毁一个已经加锁的互斥量;
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex)

🍋互斥量加锁和解锁🍋

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用pthread_mutex_lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

所以我们可以改进下上面的抢票:

int g_tictet=10000;
pthread_mutex_t mtu=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void* Run(void* args)
{string name=static_cast<const char*>(args);while(true){pthread_mutex_lock(&mtu);if(g_tictet<=0){pthread_mutex_unlock(&mtu);break;}else{cout<<"I am "<<name<<",is running  tickets"<<g_tictet<<endl;g_tictet--;}pthread_mutex_unlock(&mtu);usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i=0;i<5;++i){char* name=new char[26];snprintf(name,26,"pthread%d",i+1);pthread_create(ptids+i,nullptr,Run,name);}for(int i=0;i<5;++i){pthread_join(ptids[i],nullptr);}return 0;
}

当我们再次运行时:
在这里插入图片描述
我们发现不会出现多个线程抢占同一张票并且打印混乱的情况了。

代码中值得注意的事情有:加锁的策略是:选用的粒度一般是越细越好

🍋互斥量实现原理探究🍋

搞了这么多,那么互斥量的实现原理究竟是啥捏?

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

我们可以自己实现一份lock和unlock的伪代码

lock:movb $0,%alxchgb %al,mutexif(al寄存器的内容>0)return 0;//表示申请锁成功else挂起等待;goto lock;unlock:movb $1,%al唤醒等待mutex的线程;return 0;//表示释放锁成功

通过上面的伪代码我们可以知道当初始值mutex的值为1时,假设线程1先进行申请锁,会先将寄存器中的值改为0,然后用寄存器中的0交换mutex中的1,此时1被线程1给拿到了,假设此时线程1的时间片到了,要切换线程2执行,在切换之前先保存了线程1的上下文数据,然后切换;此时线程2从头执行将寄存器中的数值改为0,然后交换,但是唯一的1已经被线程1给拿走了,所以线程而只有挂起等待;当重新切换回线程1的时候,线程1会重新恢复上下文数据,也就是寄存器的内容会被恢复到切换前,所以判断寄存器的内容>0,申请成功。此时我们发现就算是有多个线程并发的抢占锁资源时,也只有一个线程能够申请成功,其他线程在挂起等待,因为这里面的1只有一个,并且是以交换形式进行的,可以理解这里面的1本质就是一把锁。

释放资源就更好理解了,将寄存器的值修改为1,然后唤醒等待锁的线程即可。从释放锁的那段伪代码中我们也能够看到:当多个线程申请同一把锁时,一个线程申请了锁后,虽然其他线程不能够申请了,但是却可以释放该锁


2.3 🍎可重入VS线程安全 🍎

🍋概念🍋

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

🍋常见的线程不安全/安全的情况🍋

不安全情况:

  • 1️⃣不保护共享变量的函数
  • 2️⃣函数状态随着被调用,状态发生变化的函数
  • 3️⃣返回指向静态变量指针的函数
  • 4️⃣调用线程不安全函数的函数

安全情况:

  • 1️⃣每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 2️⃣类或者接口对于线程来说都是原子操作
  • 3️⃣多个线程之间的切换不会导致该接口的执行结果存在二义性

🍋常见不可重入/可重入的情况🍋

不可重入:

  • 1️⃣调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 2️⃣调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 3️⃣可重入函数体内使用了静态的数据结构

可重入:

  • 1️⃣不使用全局变量或静态变量
  • 2️⃣不使用用malloc或者new开辟出的空间
  • 3️⃣不调用不可重入函数
  • 4️⃣不返回静态或全局数据,所有数据都有函数的调用者提供
  • 5️⃣使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

🍋可重入与线程安全联系与区别🍋

联系:

  • 1️⃣函数是可重入的,那就是线程安全的
  • 2️⃣函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 3️⃣如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

区别:

  • 1️⃣可重入函数是线程安全函数的一种
  • 2️⃣线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 3️⃣如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

2.4 🍎死锁🍎

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

🍋死锁四个必要条件🍋

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

🍋避免死锁🍋

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁避免算法有银行家算法和死锁检测算法,大家有兴趣可以自行下去研究。


3 🍑用封装使代码更加优雅 🍑

我们上面写的代码中,我们能否自己实现一个简易版本的创建线程(类似于C++11提供的线程库那样)的类呢?以及加锁和解锁能够使用RAII的思想来帮助我们完成呢?当然是可以的,我们可以自己实现一个更加优雅的代码:

mutexGuard.hpp:

#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;class mutexGurad
{
public:mutexGurad(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~mutexGurad(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};

thread.hpp:

#pragma once
#include <iostream>
#include <functional>
using namespace std;class threadProcess
{
public:enum stu{NEW,RUNNING,EXIT};template <class T>threadProcess(int num, T exe, void *args): _tid(0),_status(NEW),_exe(exe),_args(args){char name[26];snprintf(name, 26, "thread%d", num);_name = name;}static void *runHelper(void *args){threadProcess *ts = (threadProcess *)args; (*ts)();return nullptr;}void operator()() // 仿函数{if (_exe != nullptr)_exe(_args);}void Run(){int n = pthread_create(&_tid, nullptr, runHelper, this);if (n != 0)exit(-1);_status = RUNNING;}void Join(){int n = pthread_join(_tid, nullptr);if (n != 0)exit(-1);_status = EXIT;}private:string _name;pthread_t _tid;stu _status;function<void *(void *)> _exe;void *_args;
};

测试程序:

int g_tictet = 10000;
pthread_mutex_t mtu = PTHREAD_MUTEX_INITIALIZER;void *Run(void *args)
{string name = static_cast<const char *>(args);while (true){{mutexGurad mutGuard(&mtu);if (g_tictet <= 0){break;}else{cout << "I am " << name << ",is running  tickets" << g_tictet << endl;g_tictet--;}}usleep(1000);}return nullptr;
}int main()
{threadProcess thpro1(1, Run, (void *)"thread1");threadProcess thpro2(2, Run, (void *)"thread2");threadProcess thpro3(3, Run, (void *)"thread3");thpro1.Run();thpro2.Run();thpro3.Run();thpro1.Join();thpro2.Join();thpro3.Join();return 0;
}

当我们运行时:
在这里插入图片描述
我们依旧能够得到正确的结果,并且代码写起来也好看多了。除此之外,我们还可以拿到线程的其他特性,这里我就不在测试了。

相关文章:

【多线程中的线程安全问题】线程互斥

1 &#x1f351;线程间的互斥相关背景概念&#x1f351; 先来看看一些基本概念&#xff1a; 1️⃣临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。2️⃣临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区。3️⃣互斥&…...

抖音seo短视频矩阵系统源代码开发技术分享

抖音SEO短视频矩阵系统是一种通过优化技术&#xff0c;提高在抖音平台上视频的排名和曝光率的系统。以下是开发该系统的技术分享&#xff1a; 熟悉抖音平台的算法 抖音平台的算法是通过分析用户的兴趣爱好和行为习惯&#xff0c;对视频进行排序和推荐。因此&#xff0c;开发人员…...

flutter实战(01)windows桌面版 修改应用logo、名称、显示位置、显示大小

说明&#xff1a;该系列文章主要为flutter在windows桌面平台实战中遇到的一些坑。 1 修改logo 只需要在flutter项目/windows/runner/resources目录下替换原来的应用图标 app_icon.ico即可。 2 修改应用名称、显示位置、显示大小 修改flutter项目/windows/runner/main.cpp 文…...

校园基础设施资源管理

背景 自2017年起&#xff0c;为响应两会提出的“数实融合”“数字经济”“数字中国”的中国经济发展新动向&#xff0c;满足“中国教育现代化2030”战略部署&#xff0c;进一步推动“教育信息化十三五规划”的落实。这五年时间&#xff0c;各大高校致力于深化信息技术与教育教…...

Github git clone 和 git push 特别慢的解决办法

1.在本地上使用 SSH 命令无法git push 上传 github 项目 2.使用 git clone 下载项目特别慢总是加载不了 解决办法1 将 *** 的连接模式换成&#xff1a;D-i-r-e-c-t&#xff08;好像不太有用&#xff09; 后面再找找能不能再G-l-o-b-a-l 下解决该问题 解决办法 2 mac下直接设…...

【计网】TCP在可靠传输中都干了啥

文章目录 1、概述2、校验和3、序列号和确认应答机制4、重传机制4.1、介绍4.2、超时重传4.3、快速重传 5、滑动窗口协议5.1、介绍5.2、发送方的滑动窗口5.3、接收方的滑动窗口 6、流量控制7、拥塞控制7.1、介绍7.2、慢开始7.3、拥塞避免7.4、快重传和快恢复 1、概述 TCP 是面向…...

windows下载安装FFmpeg

FFmpeg是一款强大的音视频处理软件&#xff0c;下面介绍如何在windows下下载安装FFmpeg 下载 进入官网: https://ffmpeg.org/download.html, 选择Windows, 然后选择"Windows builds from gyan.dev" 在弹出的界面中找到release builds, 然后选择一个版本&#xff0…...

SwipeDelMenuLayout失效:Could not find SwipeDelMenuLayout-V1.3.0.jar

一、问题描述 最近在工作上的项目中接触到SwipeDelMenuLayout这个第三方Android开发库&#xff0c;然后我就根据网上的教程进行配置。这里先说一下我的开发环境&#xff1a;Android Studio版本是android-studio-2020.3.1.24-windows&#xff0c;gradle版本是7.0.2。 首先是在se…...

C++ 类和对象篇(零) 面向过程 和 面向对象

目录 一、面向过程 二、面向对象 三、两种编程思想的比较 四、C和C 一、面向过程 1.是什么&#xff1f; 是一种以解决问题的过程为中心的编程思想。即先分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现。 2.为什么&#xff1f; 面向过程就纯粹是分析…...

列表list

列表 列表是将数据组织在一个一维集合中&#xff0c;从这个组织方式来看&#xff0c;它与c()函数是相似的。但是&#xff0c;列表并不是将具体的值组织起来&#xff0c;而是组织R对象&#xff0c;如列表、数据框、矩阵、函数、向量等等。 列表非常好用&#xff0c;因为列表可…...

gcc编译出现bar causes a section type conflict with foo问题解决

这里bar是变量名&#xff0c;foo是函数名。 如下是charGPT给出的答复&#xff0c;结论是&#xff1a;bar和foo放在同一个section内&#xff0c;但是它们的类型不同&#xff0c;函数应该放置在一个可执行&#xff08;executable&#xff09;类型的section&#xff0c;而变量应该…...

12. Mybatis 多表查询 动态 SQL

目录 1. 数据库字段和 Java 对象不一致 2. 多表查询 3. 动态 SQL 使用 4. if 标签 5. trim 标签 6. where 标签 7. set 标签 8. foreach 标签 9. 通过注解实现 9.1 查找所有数据 9.2 通过 id 查找 1. 数据库字段和 Java 对象不一致 我们先来看一下数据库中的数…...

操作系统专栏1-内存管理from 小林coding

操作系统专栏1-内存管理 虚拟地址内存管理方案分段分页页表单级页表多级页表TLB 段页式内存管理Linux内存管理 malloc工作方式操作系统内存回收回收的内存种类 预读失败和缓存污染问题预读机制预读机制失效解决方案缓存污染 内核对虚拟内存的表示内核对内核空间的表示直接映射区…...

SpringCloud远程服务调用

下面介绍在SpringCloud中如何使用openfeign实现远程服务调用 1.在字典服务中有这么2个接口 Api(tags "数据字典接口") RestController RequestMapping("/admin/cmn/dict/") CrossOrigin public class DictController {Autowiredprivate DictService dic…...

Arcgis通过模型构建器计算几何坐标

模型 模型中&#xff0c;先添加字段&#xff0c;再计算字段 计算字段 模型的计算字段中&#xff0c;表达式是类似这样写的&#xff0c;其中Xmin表示X坐标&#xff0c;Ymin表示Y坐标 !Shape.extent.Xmin!...

java设计模式-工厂模式(下)

接java设计模式-工厂模式&#xff08;上&#xff09; 抽象工厂模式 针对耳机的生产需求&#xff0c;我们可以知道&#xff0c;刚才的工厂已经不满足了&#xff0c;因为只是生产一类产品-手机&#xff0c;但是现在我们需要的工厂类是要生产一个产品族&#xff08;手机和耳机&a…...

深蓝学院C++基础与深度解析笔记 第13章 模板

1. 函数模板 ● 使用 template 关键字引入模板&#xff1a; template<typename T> //声明&#xff1a;T模板形参void fun(T); // T 函数形参template<typename T> //定义void fun(T) {...}– 函数模板不是函数 –…...

装饰器模式——扩展系统功能

1、简介 1.1、概述 对新房进行装修并没有改变房屋用于居住的本质&#xff0c;但它可以让房子变得更漂亮、更温馨、更实用、更能满足居家的需求。在软件设计中&#xff0c;也有一种类似新房装修的技术可以对已有对象&#xff08;新房&#xff09;的功能进行扩展&#xff08;装…...

无涯教程-jQuery - jQuery.get( url, data, callback, type )方法函数

jQuery.get(url&#xff0c;[data]&#xff0c;[callback]&#xff0c;[type])方法使用GET HTTP请求从服务器加载数据。 该方法返回XMLHttpRequest对象。 jQuery.get( url, [data], [callback], [type] ) - 语法 $.get( url, [data], [callback], [type] ) 这是此方法使用的…...

【Vue3】递归组件

1. 递归组件mock数据 App.vue <template><div><Tree :data"data"></Tree></div> </template><script setup lang"ts"> import { reactive } from vue; import Tree from ./components/Tree.vue; interface Tr…...

spring:继承接口FactoryBean获取bean实例

spring框架提供接口FactoryBean获取bean实例。 实现步骤&#xff1a; 实现接口FactoryBean。 在xml文件中配置实现接口FactoryBean的类。 调用接口FactoryBean中方法getObject&#xff0c;获取bean实例。 实现接口类 package com.itheima.factory;import org.springframework…...

亚远景科技助力东风日产通过ASPICE CL2评估

热烈祝贺东风日产通过ASPICE CL2评估 近日&#xff0c;东风日产PK1B VCM热管理项目成功通过ASPICE CL2级能力评估&#xff0c;标志着东风日产在汽车电子软件研发管理体系及技术创新能力上已达到国际领先水平&#xff0c;为其全球化布局注入强劲动能。 ASPICE&#xff1a;国际竞…...

深入理解二叉搜索树:原理到实践

1.二叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树 若它的左树不为空&#xff0c;则左子树上所有节点的值都小于或等于根节点的值。若它的右树不为空&#xff0c;则右子树上所有节点的值都大于或等于根节点的…...

Neovim - 常用插件,提升体验(三)

文章目录 nvim-treelualineindent-blanklinetelescopegrug-far nvim-tree 官方文档&#xff1a;https://github.com/nvim-tree/nvim-tree.lua 以前我们都是通过 :e 的方式打开一个 buffer&#xff0c;但是这种方式需要记忆文件路径&#xff0c;因此这里可以通过 nvim-tree 插…...

8.axios Http网络请求库(1)

一句话总结 Axios 是一个基于 Promise 的 HTTP 客户端&#xff0c;用于浏览器和 Node.js&#xff0c;帮助你轻松发送请求、接收响应。 Axios is a Promise-based HTTP client for the browser and Node.js, making it easy to send requests and handle responses. &#x1…...

Python爬虫实战:研究mechanize库相关技术

1. 引言 随着互联网数据量的爆炸式增长,网络爬虫已成为数据采集和信息挖掘的重要工具。Python 作为一种功能强大且易于学习的编程语言,拥有丰富的爬虫相关库,如 Requests、BeautifulSoup、Scrapy 等。Mechanize 库作为其中的一员,特别擅长处理复杂的表单提交和会话管理,为…...

机器学习算法时间复杂度解析:为什么它如此重要?

时间复杂度的重要性 虽然scikit-learn等库让机器学习算法的实现变得异常简单&#xff08;通常只需2-3行代码&#xff09;&#xff0c;但这种便利性往往导致使用者忽视两个关键方面&#xff1a; 算法核心原理的理解缺失 忽视算法的数据适用条件 典型算法的时间复杂度陷阱 SV…...

浅聊一下,大模型应用架构 | 工程研发的算法修养系列(二)

大模型应用架构基础 AI应用演进概述 人工智能应用的发展经历了多个关键阶段&#xff0c;每个阶段都代表着技术范式的重大转变。 大语言模型基础 大语言模型(LLM)作为现代AI应用的核心组件&#xff0c;具有独特的技术特性和能力边界&#xff0c;理解这些基础对架构设计至关重要。…...

机器人夹爪的选型与ROS通讯——机器人抓取系统基础系列(六)

文章目录 前言一、夹爪的选型1.1 任务需求分析1.2 软体夹爪的选型 二、夹爪的ROS通讯2.1 夹爪的通信方式介绍2.2 串口助手测试2.3 ROS通讯节点实现 总结Reference: 前言 本文将介绍夹爪的选型方法和通讯方式。以鞋子这类操作对象为例&#xff0c;将详细阐述了对应的夹爪选型过…...

Python爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...