Python之并发编程

2023-09-21 10:00:00


并发与并行:并发是同一时间段内处理多件事情(交替执行),并行是某一时刻处理多个事情

关于进程

进程是正在运行的程序,是系统进行资源分配的最小单位

进程的内存空间

详解参考链接:进程的内存分布_进程内存_循梦的博客-CSDN博客

进程的内存空间被寄存器界定,不同进程空间之间不能互相访问,两个进程想要通信,必须通过一个中间代理来实现

在32位操作系统中,进程最大内存一般为4G虚拟内存(所谓虚拟内存即物理内存的逻辑映射,这样保证了每个进程都有相同的内存布局,减少了物理内存区段不同的干扰)

其中内核空间1G、用户空间3G

虚拟内存中,内核区段对于用户应用程序而言是禁闭的,它们用于存放操作系统的关键代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x00000000 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的
虚拟内存与物理内存的映射

用户空间中包含几个部分:

  • 栈区:全称运行时栈,或堆栈。特点是后进先出,系统一般对栈空间大小限制为8MB,一般存储环境变量、命令行变量、局部变量以及函数切换时当下的代码地址和相关寄存器的值(又叫保存现场)。还需要注意的是:栈内存的分配和释放,都是由系统规定的,我们无法干预。

    • [root@zh-ali ~]# cat  /proc/1/limits
      Limit                     Soft Limit           Hard Limit           Units     
      Max cpu time              unlimited            unlimited            seconds   
      Max file size             unlimited            unlimited            bytes     
      Max data size             unlimited            unlimited            bytes     
      Max stack size            8388608              unlimited            bytes     # stack 栈空间
      Max core file size        0                    unlimited            bytes     
      Max resident set          unlimited            unlimited            bytes     
      Max processes             6854                 6854                 processes 
      Max open files            1048576              1048576              files     
      Max locked memory         65536                65536                bytes     
      Max address space         unlimited            unlimited            bytes     
      Max file locks            unlimited            unlimited            locks     
      Max pending signals       6854                 6854                 signals   
      Max msgqueue size         819200               819200               bytes     
      Max nice priority         0                    0                    
      Max realtime priority     0                    0                    
      Max realtime timeout      unlimited            unlimited            us        
      [root@zh-ali ~]# echo $((8388608/1024))
      8192
      [root@zh-ali ~]# echo $((8192/1024))   # 8MB
      8
      
  • 堆区(heap):又称动态内存、自由内存。是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。

    • 堆内存基本特征:

      • 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
      • 相比栈内存,堆内存从下往上增长。
      • 堆内存是匿名的,只能由指针来访问。
      • 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。

      相关API:

      • 申请堆内存:malloc() / calloc() / realloc()
      • 清零堆内存:bzero()
      • 释放堆内存:free()
  • 数据段:数据段的大小在进程一开始运行就是固定的。地址从高到低,将数据段分为.bss段、.data段、.rodata段三部分。

    • .bss段专门用来存放未初始化的静态数据(static修饰的局部变量、static修饰的全局变量以及全局变量),它们会在程序刚运行时被系统初始化为0;在程序文件中,它们是没有值的。
    • .data段专门存放已经初始化的静态数据,这个初始值从程序文件中获取
    • .rodata段用来存放只读数据,即常量;比如进程中所有的字符串、字符常量、整型数据、浮点型数据等。
  • 代码段:地址从高到低,将代码段分为.text段以及.init段两部分。

    • .text段也叫正文段,用来存放用户程序代码(所有用户自定义函数)。

    • .init段用来存储系统给每一个可执行程序自动添加的初始化代码,这部分代码功能包括:环境变量的准备、命令行参数的组织和传递等,并且这部分数据被存放在栈底。

    • 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。

进程内存空间

进程通信

方法如下:

  • 管道:一个内核缓冲区,先进先出
    • 无名管道:父子进程之间,有亲缘关系的进程之间进行通信;因为管道没有名字不能确定位置,但子进程被创建时会拷贝父进程的资源,自然就知道了管道的位置
    • 命名管道:无需确定亲缘关系也能通信
  • 消息队列
    • 内核中的消息列表,可以根据自己的情况读取特定的消息
  • 信号
    • ctrl+c、ctrl+z、kill等等都是发送信号
  • 信号量:是一个计数器,用于实现进程间的互斥和通信,而不是用于存储进程间通信数据
    • P\V操作
    • 若要在进程间传递数据,需结合共享内存使用
  • 共享内存:高效的进程通信方式
    • 允许两个以上的进程通过映射共享存储区
    • 两种实现方式:内存映射、共享内存机制
    • ipcs -m:查看共享内存段
    • ipcrm:清理共享内存
  • socket套接字
    • 可以用在不同主机之间的通信

线程

线程是操作系统进行调度的最小单位,是一串指令的集合

• 线程被称为轻量级进程(Lightweight Process,LWP),是cpu调度的基本单位

组成:线程ID、当前指令指针(PC)、寄存器集合、堆栈组成

线程与进程的区别:

真正运行在cpu上的是线程,线程共享内存空间————进程的内存是独立的

一个线程只能属于一个进程————一个进程可以有多个线程,但至少得有一个线程

线程共享进程资源————进程资源独立

同一个进程的线程可直接通信————进程间通信需要中间代理

一个线程可以控制和操作同一个进程内的其他线程————进程操作子进程

一个主线程改变可能会影响其它线程

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。

  • 协程,又称微线程,纤程,英文名Coroutine。

  • 协程的作用:

    • 在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。

    • 但这一过程并不是函数调用(没有调用语句)

  • 协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力,即无法利用多核资源

进程状态模型

进程状态模型

进程的组成

进程的组成:程序控制块PCB、数据段、正文段

pcb

pcb就是一个struct(结构体),包含了许多属性,如:pid、属主、属组、优先级、状态、地址、打开的文件等等

threading模块

创建和启动

import threading
import time

def sleep(t):
    time.sleep(t)
    print(f"睡眠{t}s")

def runtime(func):
    def inner(t):
        start = time.time()
        func(t)
        end = time.time()
        print(f"共耗时{end-start}s")
    return inner

@runtime
def main(t):
    for i in range(5):
        # sleep(t)
        thread = threading.Thread(target=sleep,args=(t,))   # target传入callable对象,args传入参数(需为元组形式)
        thread.start()  # 启动时自动调用run()方法,run()方法再去调用传递进来的target

main(1)
print("ok")

join阻塞

@runtime
def main(t):
    thread_list = []
    for i in range(5):
        thread = threading.Thread(target=sleep,args=(t,))
        thread.start()
        thread_list.append(thread)
        # thread.join() # 如果放这里相当与创建一个线程就阻塞一次,并没有并发的效果

    # 所以要等所有线程全都创建启动后再join
    for t in thread_list:
        t.join()    # 线程对象内置方法,将该线程加入到上下文,阻塞当前环境上下文,直到该线程执行完成
        # 其实只有第一个join会阻塞,因为当第一个线程结束时,其它线程也差不多一起结束了

main(1)
print("ok")

设置前台/后台线程

@runtime
def main(t):
    for i in range(5):
        thread = threading.Thread(target=sleep,args=(t,))
        # 前台线程、后台线程 需在start之前设置
        # 默认是False 即 前台进程 -- 主线程等待子线程结束后才退出
        # 设置为True则为 后台进程 -- 主线程执行结束,子进程就退出
        thread.setDaemon(True)
        thread.start()

自定义线程类

更为常用的实现写法,重点在于重写run()方法

class Mythread(threading.Thread):
    def __init__(self,num):
        super().__init__()	# 继承父类初始化函数
        self.num = num

    def run(self):
        print(f"hello, num is {self.num}")

t1 = Mythread(998)
t1.start()

线程锁

线程,因为在同一个进程中共享资源,很容易发生资源争抢,产生脏数据,因此需要锁的概念

互斥锁(普通锁)

允许某一部分资源,同时只有一个线程访问

对于类似下面的情形:

import threading
import time

n = 0
def add_num(i):
    global n
    time.sleep(1)
    n += i
    print(f"num is {n}")

for i in range(10):
    t = threading.Thread(target=add_num,args=(i,))
    t.start()

# --------输出结果----------
num is 6num is 14num is 18num is 23num is 24
num is 27num is 36
num is 38


num is 45


num is 45

输出结果混乱不齐,只因为对公共资源的请求是不可控的,因此需要给公共资源加锁:

import threading
import time

n = 0
def add_num(i):
    lock.acquire()	# 加锁,返回bool类型
    global n
    time.sleep(1)
    n += i
    print(f"num is {n}")
    lock.release()	# 释放锁

lock = threading.Lock()		# 生成锁实例
for i in range(10):
    t = threading.Thread(target=add_num,args=(i,))
    t.start()

# ----或者换种写法:
def add_num(i):
    with lock:
	    global n
    	time.sleep(1)
    	n += i
	    print(f"num is {n}")
Lock Rlock 原始锁重入锁

是互斥锁的两种类型

lock1 = threading.Lock()	# 原始锁
lock2 = threading.RLock()	# 重入锁

lock1.acquire()		# 申请锁1
lock1.acquire()		# 再次申请锁1,但lock1为原始锁,在申请锁时不会判断自己是不是已经获取了这把锁,因此陷入无限等待,即死锁

lock2.acquire()
lock2.acquire()		# lock2为重入锁,申请锁时会进行判断,如果有了该锁,则立即返回,不会等待
如何避免产生死锁?
  • 尽量避免同一个线程对多个lock进行锁定

  • 多个线程对多个lock进行锁定时,尽量以相同的顺序加锁(这样需求就不会交叉)

  • 设置超时时间:lock.acquire(timeout=1.2)

信号量Semaphore

最多允许同时N个线程执行内容

相当于有多把钥匙,使用起来和lock类似

事件锁Event

根据状态位,决定是否通过事件

主线程通过决定”Flag”的值来控制其它线程中wait()方法阻塞的事件能否执行

通过发送event信号,其它的线程则等待这个信号(Event是线程间通信最常见的机制之一)

条件锁Condition

该机制使得线程等待,只有满足某条件时,才释放n个线程(可以控制释放个数)

类似于信号量和事件锁的结合

全局解释器锁 GIL

​ --Global Interpreter Lock

该锁与python语言没有关系,只是因为历史遗留问题:官方推荐的解释器cpython中的设定(Jpython无此类问题)

每个线程在执行过程中都需要先获取GIL,保证同一时刻同一进程内只有一个线程可以执行代码

GIL(全局解释器锁)是一种在CPython解释器中使用的机制,它限制了同一时刻只能有一个线程执行Python字节码的能力。这意味着在多线程的情况下,同一时刻只有一个线程能够执行Python代码,其他线程会被阻塞。

GIL的存在是因为CPython的内存管理并不是线程安全的。GIL的作用是保护Python解释器内部的数据结构免受并发访问的影响。然而,这也意味着在多线程的情况下,Python的多线程程序并不能充分利用多核处理器的优势。

由于GIL的存在,CPU密集型的Python程序在多线程下并不能获得性能的提升。但对于I/O密集型的程序,多线程依然可以提供一定的性能优势,因为在I/O操作时,线程会释放GIL,让其他线程有机会执行。

• GIL最基本的行为只有下面两个:

  • 当前执行的线程持有GIL

  • 当线程遇到io阻塞或者cpu时间片到时,会释放GIL

因此,在python中,io密集型任务适合使用:多线程、多进程(io密集型任务特点:很长时间都在等待)

计算密集型任务适合使用:多进程

多进程

创建进程:用户创建出来的所有进程都是由操作系统负责,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的

如何创建子进程?

在python中,每一个运行的程序都有一个主进程,可以利用模块中封装的方法来创建子进程————》os.fork()

os.fork

os.fork中就用来创建子进程的方法

注意:这个os.fork()方法只有在unix系统中才会有,在window下没有。

• 使用fork创建子进程后,操作系统会将当前的进程复制一份

• 原来的进程称为父进程,新创建的进程称为子进程

• 两个进程会各自互不干扰的执行下面的程序–>两个进程都会执行

• 父进程与子进程的执行顺序与系统调度有关

在子进程内,这个方法会返回0;在父进程内,这个方法会返回子进程的编号PID

​ • 返回值为大于0时,此进程为父进程,且返回的数字为子进程的PID;

​ • 当返回值为0时,此进程为子进程。

​ • 如果返回值为负数则表明创建子进程失败。

• 父进程结束时,子进程并不会随父进程立刻结束。同样,父进程不会等待子进程执行完(当然,可以用join方法阻塞父进程)。

  • 僵尸进程:子进程退出,父进程没有响应,没有调用wait或者waitpid去获取子进程的状态,那么这个子进程的进程描述符就会依然存在系统中,这种进程即僵尸进程
  • 孤儿进程:父进程退出,而子进程继续时,称为孤儿进程,这时子进程认pid为1的进程(systemd)为父进程

os.getpid():获取进程的进程号。

os.getppid():获取父进程的进程号

[root@sc-server bingfa-test]# cat os-test.py
import os, time

print("start....fork")
result = os.fork()
print("outerside pid is:", result)
if result == 0:		# 子进程返回值为0
	print("child process")
	time.sleep(60)
	print("child pid is:", os.getpid())
	print("child-parent pid is:", os.getppid())
else:				# 父进程返回值为子进程的pid
	print("parent process")
	time.sleep(60)
	print("parent pid is:", os.getpid())

multiprocessing模块

由于windows没有fork调用,python提供了multiprocessing支持跨平台版本。

创建管理进程模块:

• Process(用于创建进程模块)

• Pool(用于创建管理进程池)

• Queue(用于进程通信,资源共享)

• Value,Array(用于进程通信,资源共享)

• Pipe(用于管道通信)

• Manager(用于资源共享)

Process 类

构造方法:Process([group [, target [, name [, args [, kwargs]]]]])

• group: 线程组,目前还没有实现,库引用中提示必须是None;

• target: 要执行的方法;

• name: 进程名;

• args/kwargs: 要传入方法的参数。

示例:

from multiprocessing import Process, current_process
import time

lst = []

def task(i):
    print(current_process().name, f"start……{i}")
    time.sleep(2)
    lst.append(i)
    print(f"lst = {lst}")		# 注意:这里每个进程都只会输出一个数,因为进程之间的数据互相独立,互相隔离。
    print(current_process().name, f"end……{i}")

if __name__ == "__main__":      # 使用多进程时最好加上这行,不然会报错
    for i in range(4):
        p = Process(target=task,args=(i,))
        p.start()
自定义进程类
# 自定义进程类
class Myprocess(Process):
    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self) -> None:
        print(f"running……{self.name}")

if __name__ == "__main__":
    p1 = Myprocess("p1")
    p2 = Myprocess("p2")
    p1.start();p2.start()
进程间通信方法
  • Queue
  • Value,Array
  • Pipe
  • Manager
Manager
import time
from multiprocessing import Process, Manager


def task(i, temp, lock):
	print("start........")
	time.sleep(2)
	lock.acquire()		# 或者用with lock:
	temp[0] += 100
	print(i,"---->", temp[0])
	lock.release()

if __name__ == "__main__":
	m1 = Manager()		# 实例化Manager对象
	temp = m1.list([1,2,3])		# 创建一个共享列表对象
	lock = m1.Lock()		# 创建一个共享互斥锁,这里其实用Lock()直接生成锁实例也是可以的,锁自带共享属性吧可能
	p_list = []
	for i in range(10):
		p = Process(target=task,args=(i, temp, lock))
		p.start()
		p_list.append(p)
	[p.join() for p in p_list]		# 等待子进程运行完成再往下运行,否则父进程结束后,Manager进程也会受影响
	#父进程先退出的话,manager共享就没有用了
	print("end......")

Manager会启动一个进程,并且通过socket实现

Queue

特点是先进先出,是一种进程安全的数据结构,其进出操作都具有原子性

from multiprocessing import Process, Queue
import time

def task(i, p):
    if not p.empty():   # 队列是否为空
        time.sleep(1)
        print(i, "---->get value", p.get())     # 取数据,先进先出,所以没有脏数据

if __name__ == "__main__":
    p = Queue()         # 创建队列
    for i in range(10):
        p.put(i)        # 放数据
        q = Process(target=task, args=(i, p))
        q.start()
进程锁

multiprocessing中有threading中的同名锁,包括Lock、RLock、Semaphore、Event、Condition

用法基本相同

进程池

一般我们是通过动态创建子进程(或子线程)来实现并发服务器的,但是会存在这样一些缺点:

  1. 动态创建进程(或线程)比较耗费时间,这将导致较慢的服务器响应。

  2. 动态创建的子进程通常只用来为一个客户服务,这样导致了系统上产生大量的细微进程(或线程)。进程和线程间的切换将消耗大量CPU时间。

  3. 动态创建的子进程是当前进程的完整映像,当前进程必须谨慎的管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能。

Pool 进程池

• 进程池的作用:有效的降低频繁创建销毁线程所带来的额外开销。

通常预创建的进程数与cpu核数相等

nginx就是很明显的使用进程池的例子,worker进程就是无论有没有请求都保持监听,当手动杀死其中一个worker进程时,master进程又会创建一个新的worker进程

进程池的原理

• 进程池都是采用预创建的技术,在应用启动之初便预先创建一定数目的进程。

• 应用在运行的过程中,需要时可以从这些进程所组成的进程池里申请分配一个空闲的进程,来执行一定的任务,任务完成后,并不是将进程销毁,而是将它返还给进程池,由线程池自行管理。

• 如果进程池中预先分配的线程已经全部分配完毕,但此时又有新的任务请求,则进程池会动态的创建新的进程去适应这个请求。

• 某些时段应用并不需要执行很多的任务,导致了进程池中的线程大多处于空闲的状态,为了节省系统资源,进程池就需要动态的销毁其中的一部分空闲进程

• 进程需要一个管理者,按照一定的要求去动态的维护其中进程的数目。

from multiprocessing import current_process, Pool
import time

def task(i):
	print(current_process().name,f"start.....{i}")
	time.sleep(2)
	print(current_process().name,f'end......{i}')

if __name__ == "__main__":
	p = Pool(processes=4,maxtasksperchild=2)
	for i in range(20):
		p.apply_async(func=task,args=(i,))
	p.close()
	p.join()
	print("end......process")

协程 asyncio模块

协程编程多用于网络高并发的编程

import asyncio
import time

async def say_after(delay,what):
    print(f"test start......{what}")
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(
        say_after(4,'hello'))
    task2 = asyncio.create_task(
        say_after(5,'world'))

    print(f"started at {time.time()}")

asyncio.run(main())

小结

本节文章讲解了python中的并发编程,从操作系统层面的进程和内存入手,随后讲解了多线程、多进程、进程池以及多协程的实现,其中对线程和进程讲解较为详细,对于python协程的使用笔者还没有深入学习,毕竟并发编程也不是python的优势所在,对于并发编程,go语言或许才是目前的首选,笔者后续也可能会学习更新相关的知识,感谢大家的观看,有什么问题也欢迎留言讨论,谢谢。

更多推荐

MS COCO数据集介绍以及pycocotools使用

MSCOCO数据集介绍以及pycocotools使用1、MSCOCO数据集简介2、MSCOCO数据集目录结构3、MSCOCO标注文件格式3.1使用Python的json库查看3.2使用官方cocoAPI查看4、目标检测验证任务mAP1、MSCOCO数据集简介2、MSCOCO数据集目录结构├──coco2017:数据集根

log4j2 日志保存至数据库

文章目录概述一、springmvc工程1.创建数据库日志表2.log4j2.xml引入JDBCAppender3.定义日志管理类4.编写日志输出代码5.运行结果6.完整代码二、springboot工程1.创建数据库日志表2.log4j2.xml引入JDBCAppender3.定义日志管理类4.遗留问题5.解决办法6.完

百度SEO优化基本原理(掌握SEO基础,提高网站排名)

随着互联网的迅速发展,越来越多的企业开始意识到网站优化的重要性,其中百度SEO优化是企业不可忽视的一项工作。本文将介绍百度SEO优化的基本概念、步骤、原理、解决方法和提升网站标题优化的方法。蘑菇号-www.mooogu.cn百度SEO优化是指针对百度搜索引擎的搜索算法进行网站优化,提高网站在百度搜索结果页面上的排名。主

方案:浅析AI视频分析与视频监控技术的工厂车间智能化监管方案

一、方案背景工厂生产车间一般是从原材料到成品的流水作业,有大量器械和物料。为保障车间财产安全并提高生产效率,需要进行全面的监管。在生产制造流水线的关键工序中,不仅有作业过程监管需求,同时,也存在生产发生异常及产品质量问题的过程还原需求,需要结合直观现场与客观数据的融合分析。当前工厂车间的监管存在以下痛点:1)生产状态(

一致性思维链(SELF-CONSISTENCY IMPROVES CHAIN OF THOUGHT REASONING IN LANGUAGE MODELS)

概要思维链已经在很多任务上取得了非常显著的效果,这篇论文中提出了一种self-consistency的算法,来代替贪婪解码算法。本方法通过采样多个思维链集合,然后LLM模型生成后,选择一个最一致的答案作为最后的结果。一致性思维链认为复杂的推理问题,有不同的思维方式去解决,从而得到最终唯一答案。经过实验验证,一致性思维链

nginx 配置 ssl

1.1Nginx如果未开启SSL模块,配置Https时提示错误原因也很简单,nginx缺少http_ssl_module模块,编译安装的时候带上--with-http_ssl_module配置就行了,但是现在的情况是我的nginx已经安装过了,怎么添加模块,其实也很简单,往下看:做个说明:我的nginx的安装目录是/u

Python3.11教程6:标准库简介1——os、shutil、sys、random、time、datetime、 threading

文章目录一、文件和目录处理模块1.1os模块1.2shutil模块1.3文件通配符glob1.4stat二、sys模块2.1命令行参数列表2.2-c和-m选项2.3argparse2.3.1argparse使用逻辑2.3.2`add_argument()`语法三、数学3.1math3.2random3.3numpy生成

民安智库(第三方满意度调研公司)建立企业员工满意度测评指标体系

员工满意度是企业管理者关注的重点,企业管理层如何了解职工满意度状况?民安智库借鉴国内外研究成果,总结了企业职工满意度评价指标构建原则、研究方法及研究流程。为什么要建立员工满意度测评指标体系?在现代人力资源管理强调“以人为本”的管理理念下,企业需要尊重并重视员工的需求,并将员工视为最宝贵的、可增值的资源。企业的医务人员作

小剧场短剧影视小程序源码分享,搭建自己的短剧小程序

拥有一个属于自己的短剧小程序,是现代人追求创作梦想和与观众互动的新方式。近年来,小剧场短剧影视小程序的兴起为广大创作者提供了展示才华和与观众互动的平台。如果你也渴望搭建一个自己的短剧小程序,那么你来对地方了!在本文中,我将分享小剧场短剧影视小程序的源码,帮助你开始属于自己的短剧创作之旅。小剧场短剧影视小程序源码:为什么

leetcode 1562. 查找大小为 M 的最新分组

给你一个数组arr,该数组表示一个从1到n的数字排列。有一个长度为n的二进制字符串,该字符串上的所有位最初都设置为0。在从1到n的每个步骤i中(假设二进制字符串和arr都是从1开始索引的情况下),二进制字符串上位于位置arr[i]的位将会设为1。给你一个整数m,请你找出二进制字符串上存在长度为m的一组1的最后步骤。一组

【问题记录】解决Git上传文件到GitHub时收到 “GH001: Large files detected” 错误信息!

环境Windows11家庭中文版gitversion2.41.0.windows.1GitHub问题情况在命令行中使用git上传pdf文件到GitHub服务器时,提示了如下警告信息:原因是GitHub有一个文件大小限制,通常为100MB。如果尝试上传大于此限制的文件,GitHub将拒绝接受这个文件。如果上传大于50MB

热文推荐