Python快速入门多线程与多进程

2023-10-26

Python快速入门多线程与多进程

 

多线程

多线程的含义

进程我们可以理解为是一个可以独立运行的程序单位,比如打开一个浏览器,这就开启了一个浏览器进程;打开一个文本编辑器,这就开启了一个文本编辑器进程。但一个进程中是可以同时处理很多事情的,比如在浏览器中,我们可以在多个选项卡中打开多个页面,有的页面在播放音乐,有的页面在播放视频,有的网页在播放动画,它们可以同时运行,互不干扰。为什么能同时做到同时运行这么多的任务呢?这里就需要引出线程的概念了,其实这一个个任务,实际上就对应着一个个线程的执行。

进程呢?它就是线程的集合,进程就是由一个或多个线程构成的,线程是操作系统进行运算调度的最小单位,是进程中的一个最小运行单元。比如上面所说的浏览器进程,其中的播放音乐就是一个线程,播放视频也是一个线程,当然其中还有很多其他的线程在同时运行,这些线程的并发或并行执行最后使得整个浏览器可以同时运行这么多的任务。

了解了线程的概念,多线程就很容易理解了,多线程就是一个进程中同时执行多个线程,前面所说的浏览器的情景就是典型的多线程执行。

 

并发和并行

一个程序在计算机中运行,其底层是处理器通过运行一条条的指令来实现的。

并发,英文叫作 concurrency。它是指同一时刻只能有一条指令执行,但是多个线程的对应的指令被快速轮换地执行。比如一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。

由于处理器执行指令的速度和切换的速度非常非常快,人完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行。但微观上只是这个处理器在连续不断地在多个线程之间切换和执行,每个线程的执行一定会占用这个处理器一个时间片段,同一时刻,其实只有一个线程在执行。

 

并行,英文叫作 parallel。它是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器。不论是从宏观上还是微观上,多个线程都是在同一时刻一起执行的。

并行只能在多处理器系统中存在,如果我们的计算机处理器只有一个核,那就不可能实现并行。而并发在单处理器和多处理器系统中都是可以存在的,因为仅靠一个核,就可以实现并发。

 

举个例子,比如系统处理器需要同时运行多个线程。如果系统处理器只有一个核,那它只能通过并发的方式来运行这些线程。如果系统处理器有多个核,当一个核在执行一个线程时,另一个核可以执行另一个线程,这样这两个线程就实现了并行执行,当然其他的线程也可能和另外的线程处在同一个核上执行,它们之间就是并发执行。具体的执行方式,就取决于操作系统的调度了。

 

多线程适用场景

在一个程序进程中,有一些操作是比较耗时或者需要等待的,比如等待数据库的查询结果的返回,等待网页结果的响应。如果使用单线程,处理器必须要等到这些操作完成之后才能继续往下执行其他操作,而这个线程在等待的过程中,处理器明显是可以来执行其他的操作的。如果使用多线程,处理器就可以在某个线程等待的时候,去执行其他的线程,从而从整体上提高执行效率。

像上述场景,线程在执行过程中很多情况下是需要等待的。比如网络爬虫就是一个非常典型的例子,爬虫在向服务器发起请求之后,有一段时间必须要等待服务器的响应返回,这种任务就属于 IO 密集型任务。对于这种任务,如果我们启用多线程,处理器就可以在某个线程等待的过程中去处理其他的任务,从而提高整体的爬取效率。

但并不是所有的任务都是 IO 密集型任务,还有一种任务叫作计算密集型任务,也可以称之为 CPU 密集型任务。顾名思义,就是任务的运行一直需要处理器的参与。此时如果我们开启了多线程,一个处理器从一个计算密集型任务切换到切换到另一个计算密集型任务上去,处理器依然不会停下来,始终会忙于计算,这样并不会节省总体的时间,因为需要处理的任务的计算总量是不变的。如果线程数目过多,反而还会在线程切换的过程中多耗费一些时间,整体效率会变低。

所以,如果任务不全是计算密集型任务,我们可以使用多线程来提高程序整体的执行效率。尤其对于网络爬虫这种 IO 密集型任务来说,使用多线程会大大提高程序整体的爬取效率。

 

Python 实现多线程

在 Python 中,实现多线程的模块叫作 threading,是 Python 自带的模块。

 

Thread 直接创建子线程

首先,我们可以使用 threading.Thread 来创建一个线程,创建时需要指定 target 参数为运行的方法名称,如果被调用的方法需要传入额外的参数,则可以通过 Thread 的 args 参数来指定。示例如下:

import threading
import time


def target(second):
    print(f'Threading {threading.current_thread().name} is running')
    print(f'Threading {threading.current_thread().name} sleep {second}s')
    time.sleep(second)
    print(f'Threading {threading.current_thread().name} is ended')


print(f'Threading {threading.current_thread().name} is running')

for i in [1, 5]:
    thread = threading.Thread(target=target, args=[i])
    thread.start()

print(f'Threading {threading.current_thread().name} is ended')


##输出结果:
Threading MainThread is running
Threading Thread-1 is running
Threading Thread-1 sleep 1s
Threading Thread-2 is running Threading MainThread is ended

Threading Thread-2 sleep 5s
Threading Thread-1 is ended
Threading Thread-2 is ended

在这里我们首先声明了一个方法,叫作 target,它接收一个参数为 second,通过方法的实现可以发现,这个方法其实就是执行了一个 time.sleep 休眠操作,second 参数就是休眠秒数,其前后都 print 了一些内容,其中线程的名字我们通过 threading.current_thread().name 来获取出来,如果是主线程的话,其值就是 MainThread,如果是子线程的话,其值就是 Thread-*。

然后我们通过 Thead 类新建了两个线程,target 参数就是刚才我们所定义的方法名,args 以列表的形式传递。两次循环中,这里 i 分别就是 1 和 5,这样两个线程就分别休眠 1 秒和 5 秒,声明完成之后,我们调用 start 方法即可开始线程的运行。

观察结果我们可以发现,这里一共产生了三个线程,分别是主线程 MainThread 和两个子线程 Thread-1、Thread-2。另外我们观察到,主线程首先运行结束,紧接着 Thread-1、Thread-2 才接连运行结束,分别间隔了 1 秒和 4 秒。这说明主线程并没有等待子线程运行完毕才结束运行,而是直接退出了,有点不符合常理

 

如果我们想要主线程等待子线程运行完毕之后才退出,可以让每个子线程对象都调用下 join 方法,实现如下:

import threading
import time


def target(second):
    print(f'Threading {threading.current_thread().name} is running')
    print(f'Threading {threading.current_thread().name} sleep {second}s')
    time.sleep(second)
    print(f'Threading {threading.current_thread().name} is ended')


print(f'Threading {threading.current_thread().name} is running')

threads = []
for i in [1, 5]:
    thread = threading.Thread(target=target, args=[i])
    threads.append(thread) ##添加线程到线程列表
    thread.start()

for thread in threads:
    thread.join() #等待至线程终止。这阻塞调用线程直至线程的join()方法被调用终止-正常退出或者抛出未处理的异常-或者是可选的超时发生

print(f'Threading {threading.current_thread().name} is ended')


##输出结果:
Threading MainThread is running
Threading Thread-1 is running
Threading Thread-1 sleep 1s
Threading Thread-2 is running
Threading Thread-2 sleep 5s
Threading Thread-1 is ended
Threading Thread-2 is ended
Threading MainThread is ended

主线程必须等待子线程都运行结束,主线程才继续运行并结束。

 

继承 Thread 类创建子线程

另外,我们也可以通过继承 Thread 类的方式创建一个线程,该线程需要执行的方法写在类的 run 方法里面即可。上面的例子的等价改写为:

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, second):
        threading.Thread.__init__(self)
        self.second = second

    def run(self):
        print(f'Threading {threading.current_thread().name} is running')
        print(f'Threading {threading.current_thread().name} sleep {self.second}s')
        time.sleep(self.second)
        print(f'Threading {threading.current_thread().name} is ended')


print(f'Threading {threading.current_thread().name} is running')
threads = []

for i in [1, 5]:
    thread = MyThread(i)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f'Threading {threading.current_thread().name} is ended')


##输出结果:
Threading MainThread is running
Threading Thread-1 is running
Threading Thread-1 sleep 1s
Threading Thread-2 is running
Threading Thread-2 sleep 5s
Threading Thread-1 is ended
Threading Thread-2 is ended
Threading MainThread is ended

 

守护线程

在线程中有一个叫作守护线程的概念,如果一个线程被设置为守护线程,那么意味着这个线程是“不重要”的,这意味着,如 果主线程结束了而该守护线程还没有运行完,那么它将会被强制结束。在 Python 中我们可以通过 setDaemon 方法来将某个线程设置为守护线程。

import threading
import time


def target(second):
    print(f'Threading {threading.current_thread().name} is running')
    print(f'Threading {threading.current_thread().name} sleep {second}s')
    time.sleep(second)
    print(f'Threading {threading.current_thread().name} is ended')


print(f'Threading {threading.current_thread().name} is running')

t1 = threading.Thread(target=target, args=[2])
t1.start()

t2 = threading.Thread(target=target, args=[5])
t2.setDaemon(True)
t2.start()

print(f'Threading {threading.current_thread().name} is ended')


##输出结果:
Threading MainThread is running
Threading Thread-1 is running
Threading Thread-1 sleep 2s
Threading Thread-2 is running Threading MainThread is ended

Threading Thread-2 sleep 5s
Threading Thread-1 is ended

通过 setDaemon 方法将 t2 设置为了守护线程,这样主线程在运行完毕时,t2 线程会随着线程t1的结束而结束。

注意:这里并没有调用 join 方法,如果我们让 t1 和 t2 都调用 join 方法,主线程就会仍然等待各个子线程执行完毕再退出,不论其是否是守护线程。

import threading
import time

def target(second):
    print(f'Threading {threading.current_thread().name} is running')
    print(f'Threading {threading.current_thread().name} sleep {second}s')
    time.sleep(second)
    print(f'Threading {threading.current_thread().name} is ended')


print(f'Threading {threading.current_thread().name} is running')

t1 = threading.Thread(target=target, args=[2])
t1.start()

t2 = threading.Thread(target=target, args=[5])
t2.setDaemon(True)
t2.start()

t1.join()        ##给t1和t2线程都执行join()方法
t2.join()

print(f'Threading {threading.current_thread().name} is ended')

##输出结果:
Threading MainThread is running
Threading Thread-1 is running
Threading Thread-1 sleep 2s
Threading Thread-2 is running
Threading Thread-2 sleep 5s
Threading Thread-1 is ended
Threading Thread-2 is ended
Threading MainThread is ended

 

互斥锁

在一个进程中的多个线程是共享资源的,比如在一个进程中,有一个全局变量 count 用来计数,现在我们声明多个线程,每个线程运行时都给 count 加 1,让我们来看看效果如何,代码实现如下:

import threading
import time

count = 0

class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global count
        temp = count + 1
        time.sleep(0.001)
        count = temp

threads = []
for _ in range(10000):
    thread = MyThread()
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()
print(f'Final count: {count}')

##输出结果:
Final count: 65

注意,此处原代码中是声明了1000个线程,但最终展示的效果不太好,所以此处将之改成了10000个线程。

在这里,我们声明了 10000 个线程,每个线程都是现取到当前的全局变量 count 值,然后休眠一小段时间,然后对 count 赋予新的值。

 

那这样,按照常理来说,最终的 count 值应该为 10000。但最后的结果居然只有 65,而且多次运行或者换个环境运行结果是不同的。这是为什么呢?

因为 count 这个值是共享的,每个线程都可以在执行 temp = count 这行代码时拿到当前 count 的值,但是这些线程中的一些线程可能是并发或者并行执行的,这就导致不同的线程拿到的可能是同一个 count 值,最后导致有些线程的 count 的加 1 操作并没有生效,导致最后的结果偏小

 

所以,如果多个线程同时对某个数据进行读取或修改,就会出现不可预料的结果。为了避免这种情况,我们需要对多个线程进行同步,要实现同步,我们可以对需要操作的数据进行加锁保护,这里就需要用到 threading.Lock 了。

加 锁保护是什么意思呢?就是说,某个线程在对数据进行操作前,需要先加锁,这样其他的线程发现被加锁了之后,就无法继续向下执行,会一直等待锁被释放,只有 加锁的线程把锁释放了,其他的线程才能继续加锁并对数据做修改,修改完了再释放锁。这样可以确保同一时间只有一个线程操作数据,多个线程不会再同时读取和 修改同一个数据,这样最后的运行结果就是对的了。

我们可以将代码修改为如下内容:

import threading
import time

count = 0

class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        global count
        lock.acquire()   ##加锁
        temp = count + 1
        time.sleep(0.001)
        count = temp
        lock.release()   ##释放锁

lock = threading.Lock() ##声明一个锁对象
threads = []
for _ in range(10000):
    thread = MyThread()
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()
print(f'Final count: {count}')


##输出结果:
Final count: 1000

在这里我们声明了一个 lock 对象,其实就是 threading.Lock 的一个实例,然后在 run 方法里面,获取 count 前先加锁,修改完 count 之后再释放锁,这样多个线程就不会同时获取和修改 count 的值了。

关于 Python 多线程的内容,这里暂且先介绍这些,关于 theading 更多的使用方法,如信号量、队列等,可以参考官方文档:https://docs.python.org/zh-cn/3.7/library/threading.html#module-threading

Python 多线程的问题

由于 Python 中 GIL 的限制,导致不论是在单核还是多核条件下,在同一时刻只能运行一个线程,导致 Python 多线程无法发挥多核并行的优势。GIL 全称为 Global Interpreter Lock,中文翻译为全局解释器锁,其最初设计是出于数据安全而考虑的。

在 Python 多线程下,每个线程的执行方式如下:

  • 获取 GIL
  • 执行对应线程的代码
  • 释放 GIL

可 见,某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是通行证,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许执行。这样就会导致,即使是多核条件下,一个 Python 进程下的多个线程,同一时刻也只能执行一个线程。

不过对于爬虫这种 IO 密集型任务来说,这个问题影响并不大。而对于计算密集型任务来说,由于 GIL 的存在,多线程总体的运行效率相比可能反而比单线程更低。

 

多进程

多进程的含义

进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。

顾名思义,多进程就是启用多个进程同时运行。由于进程是线程的集合,而且进程是由一个或多个线程构成的,所以多进程的运行意味着有大于或等于进程数量的线程在运行

 

Python 多进程的优势

 由于进程中 GIL 的存在,Python 中的多线程并不能很好地发挥多核优势,一个进程中的多个线程,在同一时刻只能有一个线程运行。而对于多进程来说,每个进程都有属于自己的 GIL,所以,在多核处理器下,多进程的运行是不会受 GIL 的影响的。因此,多进程能更好地发挥多核的优势。

当然,对于爬虫这种 IO 密集型任务来说,多线程和多进程影响差别并不大。对于计算密集型任务来说,Python 的多进程相比多线程,其多核运行效率会有成倍的提升。

总的来说,Python 的多进程整体来看是比多线程更有优势的。所以,在条件允许的情况下,能用多进程就尽量用多进程。

不过值得注意的是,由于进程是系统进行资源分配和调度的一个独立单位,所以各个进程之间的数据是无法共享的,如多个进程无法共享一个全局变量,进程之间的数据共享需要有单独的机制来实现。

 

多进程的实现

在 Python 中也有内置的库来实现多进程,它就是 multiprocessing。

multiprocessing 提供了一系列的组件,如 Process(进程)、Queue(队列)、Semaphore(信号量)、Pipe(管道)、Lock(锁)、Pool(进程池)等,接下来让我们来了解下它们的使用方法。

 

直接使用 Process 类

在 multiprocessing 中,每一个进程都用一个 Process 类来表示。它的 API 调用如下:

Process([group [, target [, name [, args [, kwargs]]]]])
  • target 表示调用对象,你可以传入方法的名字。
  • args 表示被调用对象的位置参数元组,比如 target 是函数 func,他有两个参数 m,n,那么 args 就传入 [m, n] 即可。
  • kwargs 表示调用对象的字典。
  • name 是别名,相当于给这个进程取一个名字。
  • group 分组。
import multiprocessing

def process(index):
    print(f'Process: {index}')

if __name__ == '__main__':
    for i in range(5):
        p = multiprocessing.Process(target=process, args=(i,)) #此处(i,) 或[i]都行
        p.start()


##输出结果:
Process: 0
Process: 2
Process: 1
Process: 3
Process: 4

这是一个实现多进程最基础的方式:通过创建 Process 来新建一个子进程,其中 target 参数传入方法名,args 是方法的参数,是以元组的形式传入,其和被调用的方法 process 的参数是一一对应的。

注意:这里 args 必须要是一个元组,如果只有一个参数,那也要在元组第一个元素后面加一个逗号,如果没有逗号则和单个元素本身没有区别,无法构成元组,导致参数传递出现问题。

创建完进程之后,我们通过调用 start 方法即可启动进程了。

可以看到,我们运行了 5 个子进程,每个进程都调用了 process 方法。process 方法的 index 参数通过 Process 的 args 传入,分别是 0~4 这 5 个序号,最后打印出来,5 个子进程运行结束。

 

由于进程是 Python 中最小的资源分配单元,因此这些进程和线程不同,各个进程之间的数据是不会共享的,每启动一个进程,都会独立分配资源。另外,在当前 CPU 核数足够的情况下,这些不同的进程会分配给不同的 CPU 核来运行,实现真正的并行执行。

multiprocessing 还提供了几个比较有用的方法,如我们可以通过 cpu_count 的方法来获取当前机器 CPU 的核心数量,通过 active_children 方法获取当前还在运行的所有进程

import multiprocessing
import time

def process(index):
    time.sleep(index)
    print(f'Process: {index}')

if __name__ == '__main__':
    for i in range(5):
        p = multiprocessing.Process(target=process, args=[i])  ## 注意,此处[i] 或(i,)都行
        p.start()

    print(f'CPU number: {multiprocessing.cpu_count()}')

    for p in multiprocessing.active_children():
        print(f'Child process name: {p.name} id: {p.pid}')

    print('Process Ended')

##输出结果:
CPU number: 16
Child process name: Process-5 id: 24888
Child process name: Process-4 id: 24868
Child process name: Process-2 id: 24828
Child process name: Process-3 id: 24848
Child process name: Process-1 id: 24820
Process Ended
Process: 0
Process: 1
Process: 2
Process: 3
Process: 4

在上面的例子中我们通过 cpu_count 成功获取了 CPU 核心的数量:16个,当然不同的机器结果可能不同。

另外我们还通过 active_children 获取到了当前正在活跃运行的进程列表。然后我们遍历了每个进程,并将它们的名称和进程号打印出来了,这里进程号直接使用 pid 属性即可获取,进程名称直接通过 name 属性即可获取。

 

继承 Process 类

在上面的例子中,我们创建进程是直接使用 Process 这个类来创建的,这是一种创建进程的方式。不过,创建进程的方式不止这一种,同样,我们也可以像线程 Thread 一样来通过继承的方式创建一个进程类,进程的基本操作我们在子类的 run 方法中实现即可。

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self, loop):
        Process.__init__(self)
        self.loop = loop

    def run(self):
        for count in range(self.loop):
            time.sleep(1)
            print(f'Pid: {self.pid}, LoopCount: {count}')

if __name__ == '__main__':
    for i in range(2, 5):
        p = MyProcess(i)
        p.start()


##输出结果:
Pid: 29268, LoopCount: 0Pid: 29276, LoopCount: 0
Pid: 29296, LoopCount: 0

Pid: 29268, LoopCount: 1Pid: 29296, LoopCount: 1
Pid: 29276, LoopCount: 1

Pid: 29296, LoopCount: 2Pid: 29276, LoopCount: 2
Pid: 29296, LoopCount: 3

我们首先声明了一个构造方法,这个方法接收一个 loop 参数,代表循环次数,并将其设置为全局变量。在 run 方法中,又使用这个 loop 变量循环了 loop 次并打印了当前的进程号和循环次数。

在调用时,我们用 range 方法得到了 2、3、4 三个数字,并把它们分别初始化了 MyProcess 进程,然后调用 start 方法将进程启动起来。

注意:这里进程的执行逻辑需要在 run 方法中实现,启动进程需要调用 start 方法,调用之后 run 方法便会执行。

 

可以看到,三个进程分别打印出了 2、3、4 条结果,即进程 29268打印了 2 次 结果,进程 29276 打印了 3 次结果,进程 29296打印了 4 次结果。

注意,这里的进程 pid 代表进程号,不同机器、不同时刻运行结果可能不同。

通过上面的方式,我们也非常方便地实现了一个进程的定义。为了复用方便,我们可以把一些方法写在每个进程类里封装好,在使用时直接初始化一个进程类运行即可。

 

守护进程

在多进程中,同样存在守护进程的概念,如果一个进程被设置为守护进程,当父进程结束后,子进程会自动被终止,我们可以通过设置 daemon 属性来控制是否为守护进程。

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self, loop):
        Process.__init__(self)
        self.loop = loop

    def run(self):
        for count in range(self.loop):
            time.sleep(1)
            print(f'Pid: {self.pid} LoopCount: {count}')

if __name__ == '__main__':
    for i in range(2, 5):
        p = MyProcess(i)
        p.daemon = True
        p.start()

    print('Main Process ended')

##输出结果:
Main Process ended

结果很简单,因为主进程没有做任何事情,直接输出一句话结束,所以在这时也直接终止了子进程的运行。这样可以有效防止无控制地生成子进程。这样的写法可以让我们在主进程运行结束后无需额外担心子进程是否关闭,避免了独立子进程的运行。

 

进程等待

上面的运行效果其实不太符合我们预期:主进程运行结束时,子进程(守护进程)也都退出了,子进程什么都没来得及执行。

能不能让所有子进程都执行完了然后再结束呢?当然是可以的,只需要加入 join 方法即可,我们可以将代码改写如下:

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self, loop):
        Process.__init__(self)
        self.loop = loop

    def run(self):
        for count in range(self.loop):
            time.sleep(1)
            print(f'Pid: {self.pid} LoopCount: {count}')

if __name__ == '__main__':
    processes = []

    for i in range(2, 5):
        p = MyProcess(i)
        processes.append(p)
        p.daemon = True
        p.start()

    for p in processes:
        p.join()

    print('Main Process ended')


##输出结果:
Pid: 35900 LoopCount: 0Pid: 35908 LoopCount: 0Pid: 13964 LoopCount: 0


Pid: 35908 LoopCount: 1Pid: 13964 LoopCount: 1
Pid: 35900 LoopCount: 1

Pid: 35900 LoopCount: 2Pid: 13964 LoopCount: 2

Pid: 13964 LoopCount: 3
Main Process ended

在调用 start 和 join 方法后,父进程就可以等待所有子进程都执行完毕后,再打印出结束的结果。

默认情况下,join 是无限期的。也就是说,如果有子进程没有运行完毕,主进程会一直等待。这种情况下,如果子进程出现问题陷入了死循环,主进程也会无限等待下去。怎么解决这 个问题呢?可以给 join 方法传递一个超时参数,代表最长等待秒数。如果子进程没有在这个指定秒数之内完成,会被强制返回,主进程不再会等待。也就是说这个参数设置了主进程等待该 子进程的最长时间。

例如这里我们传入 1,代表最长等待 1 秒,代码改写如下:

from multiprocessing import Process
import time

class MyProcess(Process):
    def __init__(self, loop):
        Process.__init__(self)
        self.loop = loop

    def run(self):
        for count in range(self.loop):
            time.sleep(1)
            print(f'Pid: {self.pid} LoopCount: {count}')

if __name__ == '__main__':
    processes = []

    for i in range(2, 5):
        p = MyProcess(i)
        processes.append(p)
        p.daemon = True
        p.start()

    for p in processes:
        p.join(1)

    print('Main Process ended')

##输出结果:
Pid: 35156 LoopCount: 0
Pid: 35184 LoopCount: 0Pid: 35148 LoopCount: 0

Pid: 35148 LoopCount: 1Pid: 35184 LoopCount: 1
Pid: 35156 LoopCount: 1

Main Process ended

可以看到,有的子进程本来要运行 3 秒,结果运行 1 秒就被强制返回了,由于是守护进程,该子进程被终止了。

 

终止进程

当然,终止进程不止有守护进程这一种做法,我们也可以通过 terminate 方法来终止某个子进程,另外我们还可以通过 is_alive 方法判断进程是否还在运行。

import multiprocessing
import time

def process():
    print('Starting')
    time.sleep(5)
    print('Finished')

if __name__ == '__main__':
    p = multiprocessing.Process(target=process)
    print('Before:', p, p.is_alive())

    p.start()
    print('During:', p, p.is_alive())

    p.terminate()
    print('Terminate:', p, p.is_alive())

    p.join()
    print('Joined:', p, p.is_alive())

##输出结果:
Before: <Process(Process-1, initial)> False
During: <Process(Process-1, started)> True
Terminate: <Process(Process-1, started)> True
Joined: <Process(Process-1, stopped[SIGTERM])> False

在上面的例子中,我们用 Process 创建了一个进程,接着调用 start 方法启动这个进程,然后调用 terminate 方法将进程终止,最后调用 join 方法。另外,在进程运行不同的阶段,我们还通过 is_alive 方法判断当前进程是否还在运行。

注意:在调用 terminate 方法之后,我们用 is_alive 方法获取进程的状态发现依然还是运行状态。在调用 join 方法之后,is_alive 方法获取进程的运行状态才变为终止状态。

所以,在调用 terminate 方法之后,记得要调用一下 join 方法,这里调用 join 方法可以为进程提供时间来更新对象状态,用来反映出最终的进程终止效果。

 

进程互斥锁

在上面的一些实例中(进程等待的例子中),我们可能会遇到如下的运行结果:

##输出结果:
Pid: 35156 LoopCount: 0
Pid: 35184 LoopCount: 0Pid: 35148 LoopCount: 0

Pid: 35148 LoopCount: 1Pid: 35184 LoopCount: 1
Pid: 35156 LoopCount: 1

Main Process ended

我们发现,有的输出结果没有换行。这是什么原因造成的呢?

这种情况是由多个进程并行执行导致的,两个进程同时进行了输出,结果第一个进程的换行没有来得及输出,第二个进程就输出了结果,导致最终输出没有换行。

 

那如何来避免这种问题?

如果我们能保证,多个进程运行期间的任一时间,只能一个进程输出,其他进程等待,等刚才那个进程输出完毕之后,另一个进程再进行输出,这样就不会出现输出没有换行的现象了。

这种解决方案实际上就是实现了进程互斥,避免了多个进程同时抢占临界区(输出)资源。我们可以通过 multiprocessing 中的 Lock 来实现。Lock,即锁,在一个进程输出时,加锁,其他进程等待。等此进程执行结束后,释放锁,其他进程可以进行输出。

 

我们首先实现一个不加锁的实例,代码如下:

from multiprocessing import Process, Lock
import time

class MyProcess(Process):
    def __init__(self, loop, lock):
        Process.__init__(self)
        self.loop = loop
        self.lock = lock

    def run(self):
        for count in range(self.loop):
            time.sleep(0.1)
            # self.lock.acquire()
            print(f'Pid: {self.pid} LoopCount: {count}')
            # self.lock.release()

if __name__ == '__main__':
    lock = Lock()
    for i in range(10, 15):
        p = MyProcess(i, lock)
        p.start()

##输出结果:
Pid: 51428 LoopCount: 0Pid: 51448 LoopCount: 0

Pid: 51392 LoopCount: 0Pid: 51388 LoopCount: 0
Pid: 51460 LoopCount: 0

Pid: 51448 LoopCount: 1
Pid: 51428 LoopCount: 1
Pid: 51392 LoopCount: 1
Pid: 51460 LoopCount: 1Pid: 51388 LoopCount: 1

Pid: 51448 LoopCount: 2Pid: 51428 LoopCount: 2

Pid: 51392 LoopCount: 2
Pid: 51460 LoopCount: 2Pid: 51388 LoopCount: 2

Pid: 51428 LoopCount: 3Pid: 51448 LoopCount: 3

Pid: 51388 LoopCount: 3Pid: 51460 LoopCount: 3Pid: 51392 LoopCount: 3


Pid: 51448 LoopCount: 4Pid: 51428 LoopCount: 4

Pid: 51388 LoopCount: 4
Pid: 51460 LoopCount: 4Pid: 51392 LoopCount: 4

Pid: 51428 LoopCount: 5Pid: 51448 LoopCount: 5

Pid: 51460 LoopCount: 5
Pid: 51388 LoopCount: 5Pid: 51392 LoopCount: 5

Pid: 51428 LoopCount: 6Pid: 51448 LoopCount: 6

Pid: 51388 LoopCount: 6
Pid: 51392 LoopCount: 6Pid: 51460 LoopCount: 6

Pid: 51428 LoopCount: 7Pid: 51448 LoopCount: 7

Pid: 51460 LoopCount: 7Pid: 51388 LoopCount: 7

Pid: 51392 LoopCount: 7
Pid: 51448 LoopCount: 8
Pid: 51428 LoopCount: 8
Pid: 51388 LoopCount: 8Pid: 51460 LoopCount: 8

Pid: 51392 LoopCount: 8
Pid: 51448 LoopCount: 9Pid: 51428 LoopCount: 9

Pid: 51460 LoopCount: 9Pid: 51388 LoopCount: 9
Pid: 51392 LoopCount: 9

Pid: 51428 LoopCount: 10
Pid: 51460 LoopCount: 10
Pid: 51392 LoopCount: 10Pid: 51388 LoopCount: 10

Pid: 51428 LoopCount: 11
Pid: 51388 LoopCount: 11
Pid: 51392 LoopCount: 11
Pid: 51392 LoopCount: 12
Pid: 51388 LoopCount: 12
Pid: 51392 LoopCount: 13

可以看到运行结果中有些输出已经出现了不换行的问题。去掉上述代码中的注释,重新运行,输出结果如下:

Pid: 50820 LoopCount: 0
Pid: 50752 LoopCount: 0
Pid: 50776 LoopCount: 0
Pid: 50832 LoopCount: 0
Pid: 50792 LoopCount: 0
Pid: 50792 LoopCount: 1
Pid: 50832 LoopCount: 1
Pid: 50752 LoopCount: 1
Pid: 50820 LoopCount: 1
Pid: 50776 LoopCount: 1
Pid: 50752 LoopCount: 2
Pid: 50792 LoopCount: 2
Pid: 50820 LoopCount: 2
Pid: 50832 LoopCount: 2
Pid: 50776 LoopCount: 2
Pid: 50792 LoopCount: 3
Pid: 50832 LoopCount: 3
Pid: 50776 LoopCount: 3
Pid: 50820 LoopCount: 3
Pid: 50752 LoopCount: 3
Pid: 50832 LoopCount: 4
Pid: 50752 LoopCount: 4
Pid: 50792 LoopCount: 4
Pid: 50776 LoopCount: 4
Pid: 50820 LoopCount: 4
Pid: 50792 LoopCount: 5
Pid: 50752 LoopCount: 5
Pid: 50776 LoopCount: 5
Pid: 50820 LoopCount: 5
Pid: 50832 LoopCount: 5
Pid: 50832 LoopCount: 6
Pid: 50792 LoopCount: 6
Pid: 50820 LoopCount: 6
Pid: 50776 LoopCount: 6
Pid: 50752 LoopCount: 6
Pid: 50792 LoopCount: 7
Pid: 50820 LoopCount: 7
Pid: 50776 LoopCount: 7
Pid: 50752 LoopCount: 7
Pid: 50832 LoopCount: 7
Pid: 50820 LoopCount: 8
Pid: 50792 LoopCount: 8
Pid: 50832 LoopCount: 8
Pid: 50752 LoopCount: 8
Pid: 50776 LoopCount: 8
Pid: 50832 LoopCount: 9
Pid: 50776 LoopCount: 9
Pid: 50752 LoopCount: 9
Pid: 50792 LoopCount: 9
Pid: 50820 LoopCount: 9
Pid: 50832 LoopCount: 10
Pid: 50820 LoopCount: 10
Pid: 50776 LoopCount: 10
Pid: 50752 LoopCount: 10
Pid: 50752 LoopCount: 11
Pid: 50776 LoopCount: 11
Pid: 50820 LoopCount: 11
Pid: 50752 LoopCount: 12
Pid: 50820 LoopCount: 12
Pid: 50752 LoopCount: 13

这时输出效果就正常了。所以,在访问一些临界区资源时,使用 Lock 可以有效避免进程同时占用资源而导致的一些问题。

信号量

进程互斥锁可以使同一时刻只有一个进程能访问共享资源,如上面的例子所展示的那样,在同一时刻只能有一个进程输出结果。但有时候我们需要允许多个进程来访问共享资源,同时还需要限制能访问共享资源的进程的数量。

这种需求该如何实现呢?可以用信号量,信号量是进程同步过程中一个比较重要的角色。它可以控制临界资源的数量,实现多个进程同时访问共享资源,限制进程的并发量。可以用 multiprocessing 库中的 Semaphore 来实现信号量。

 

那么接下来我们就用一个实例来演示一下进程之间利用 Semaphore 做到多个进程共享资源,同时又限制同时可访问的进程数量,代码如下: // 在windows环境下,运行后,未得到预期结果。

from multiprocessing import Process, Semaphore, Lock, Queue
import time

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()

class Consumer(Process):
    def run(self):
        global buffer, empty, full, lock
        while True:
            full.acquire()
            lock.acquire()
            buffer.get()
            print('Consumer pop an element')
            time.sleep(1)
            lock.release()
            empty.release()

class Producer(Process):
    def run(self):
        global buffer, empty, full, lock
        while True:
            empty.acquire()
            lock.acquire()
            buffer.put(1)
            print('Producer append an element')
            time.sleep(1)
            lock.release()
            full.release()

if __name__ == '__main__':
    p = Producer()
    c = Consumer()
    p.daemon = c.daemon = True
    p.start()
    c.start()
    p.join()
    c.join()
    print('Main Process Ended')

##输出结果:
    Producer append an element
    Producer append an element
    Consumer pop an element
    Consumer pop an element
    Producer append an element
    Producer append an element
    Consumer pop an element
    Consumer pop an element
    Producer append an element
    Producer append an element
    Consumer pop an element
    Consumer pop an element
    Producer append an element
    Producer append an element

如上代码实现了经典的生产者和消费者问题。它定义了两个进程类,一个是消费者,一个是生产者。

另外,这里使用 multiprocessing 中的 Queue 定义了一个共享队列,然后定义了两个信号量 Semaphore,一个代表缓冲区空余数,一个表示缓冲区占用数。

生产者 Producer 使用 acquire 方法来占用一个缓冲区位置,缓冲区空闲区大小减 1,接下来进行加锁,对缓冲区进行操作,然后释放锁,最后让代表占用的缓冲区位置数量加 1,消费者则相反。

我们发现两个进程在交替运行,生产者先放入缓冲区物品,然后消费者取出,不停地进行循环。 你可以通过上面的例子来体会信号量 Semaphore 的用法,通过 Semaphore 我们很好地控制了进程对资源的并发访问数量。

队列

在上面的例子中我们使用 Queue 作为进程通信的共享队列使用。

而如果我们把上面程序中的 Queue 换成普通的 list,是完全起不到效果的,因为进程和进程之间的资源是不共享的。即使在一个进程中改变了这个 list,在另一个进程也不能获取到这个 list 的状态,所以声明全局变量对多进程是没有用处的。

那进程如何共享数据呢?可以用 Queue,即队列。当然这里的队列指的是 multiprocessing 里面的 Queue。

 

依然用上面的例子,我们一个进程向队列中放入随机数据,然后另一个进程取出数据。  // 在windows环境下,运行后,未得到预期结果。

from multiprocessing import Process, Semaphore, Lock, Queue
import time
from random import random

buffer = Queue(10)
empty = Semaphore(2)
full = Semaphore(0)
lock = Lock()


class Consumer(Process):
    def run(self):
        global buffer, empty, full, lock
        while True:
            full.acquire()
            lock.acquire()
            print(f'Consumer get {buffer.get()}')
            time.sleep(1)
            lock.release()
            empty.release()


class Producer(Process):
    def run(self):
        global buffer, empty, full, lock
        while True:
            empty.acquire()
            lock.acquire()
            num = random()
            print(f'Producer put {num}')
            buffer.put(num)
            time.sleep(1)
            lock.release()
            full.release()


if __name__ == '__main__':
    p = Producer()
    c = Consumer()
    p.daemon = c.daemon = True
    p.start()
    c.start()
    p.join()
    c.join()
    print('Main Process Ended')

##输出结果:
    Producer put  0.719213647437
    Producer put  0.44287326683
    Consumer get 0.719213647437
    Consumer get 0.44287326683
    Producer put  0.722859424381
    Producer put  0.525321338921
    Consumer get 0.722859424381
    Consumer get 0.525321338921

在上面的例子中我们声明了两个进程,一个进程为生产者 Producer,另一个为消费者 Consumer,生产者不断向 Queue 里面添加随机数,消费者不断从队列里面取随机数。

生产者在放数据的时候调用了 Queue 的 put 方法,消费者在取的时候使用了 get 方法,这样我们就通过 Queue 实现两个进程的数据共享了。

 

管道

刚才我们使用 Queue 实现了进程间的数据共享,那么进程之间直接通信,如收发信息,用什么比较好呢?可以用 Pipe,管道。

管道,我们可以把它理解为两个进程之间通信的通道。管道可以是单向的,即 half-duplex:一个进程负责发消息,另一个进程负责收消息;也可以是双向的 duplex,即互相收发消息。

默认声明 Pipe 对象是双向管道,如果要创建单向管道,可以在初始化的时候传入 deplex 参数为 False。

from multiprocessing import Process, Pipe


class Consumer(Process):
    def __init__(self, pipe):
        Process.__init__(self)
        self.pipe = pipe

    def run(self):
        self.pipe.send('Consumer Words')
        print(f'Consumer Received: {self.pipe.recv()}')

class Producer(Process):
    def __init__(self, pipe):
        Process.__init__(self)
        self.pipe = pipe

    def run(self):
        print(f'Producer Received: {self.pipe.recv()}')
        self.pipe.send('Producer Words')

if __name__ == '__main__':
    pipe = Pipe()
    p = Producer(pipe[0])
    c = Consumer(pipe[1])
    p.daemon = c.daemon = True
    p.start()
    c.start()
    p.join()
    c.join()
    print('Main Process Ended')

##输出结果:
Producer Received: Consumer Words
Consumer Received: Producer Words
Main Process Ended

在这个例子里我们声明了一个默认为双向的管道,然后将管道的两端分别传给两个进程。两个进程互相收发。

管道 Pipe 就像进程之间搭建的桥梁,利用它我们就可以很方便地实现进程间通信了。

 

进程池

在前面,我们讲了可以使用 Process 来创建进程,同时也讲了如何用 Semaphore 来控制进程的并发执行数量。

假如现在我们遇到这么一个问题,我有 10000 个任务,每个任务需要启动一个进程来执行,并且一个进程运行完毕之后要紧接着启动下一个进程,同时我还需要控制进程的并发数量,不能并发太高,不然 CPU 处理不过来(如果同时运行的进程能维持在一个最高恒定值当然利用率是最高的)。

 

那么我们该如何来实现这个需求呢?

用 Process 和 Semaphore 可以实现,但是实现起来比较烦琐。而这种需求在平时又是非常常见的。此时,我们就可以派上进程池了,即 multiprocessing 中的 Pool。

Pool 可以提供指定数量的进程,供用户调用,当有新的请求提交到 pool 中时,如果池还没有满,就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行它。

 

我们用一个实例来实现一下,代码如下:

from multiprocessing import Pool
import time

def function(index):
    print(f'Start process: {index}')
    time.sleep(3)
    print(f'End process {index}', )

if __name__ == '__main__':
    pool = Pool(processes=3)
    for i in range(4):
        pool.apply_async(function, args=(i,))

    print('Main Process started')
    pool.close()
    pool.join()
    print('Main Process ended')

##输出结果:
Main Process started
Start process: 0
Start process: 1
Start process: 2
End process 2End process 1End process 0


Start process: 3
End process 3
Main Process ended

在这个例子中我们声明了一个大小为 3 的进程池,通过 processes 参数来指定,如果不指定,那么会自动根据处理器内核来分配进程数。接着我们使用 apply_async 方法将进程添加进去,args 可以用来传递参数。

 

进程池大小为 3,所以最初可以看到有 3 个进程同时执行,第4个进程在等待,在有进程运行完毕之后,第4个进程马上跟着运行,出现了如上的运行效果。

最后,我们要记得调用 close 方法来关闭进程池,使其不再接受新的任务,然后调用 join 方法让主进程等待子进程的退出,等子进程运行完毕之后,主进程接着运行并结束。

 

不过上面的写法多少有些烦琐,这里再介绍进程池一个更好用的 map 方法,可以将上述写法简化很多。

map 方法是怎么用的呢?第一个参数就是要启动的进程对应的执行方法,第 2 个参数是一个可迭代对象,其中的每个元素会被传递给这个执行方法。

举个例子:现在我们有一个 list,里面包含了很多 URL,另外我们也定义了一个方法用来抓取每个 URL 内容并解析,那么我们可以直接在 map 的第一个参数传入方法名,第 2 个参数传入 URL 数组。

 

我们用一个实例来感受一下:

from multiprocessing import Pool
import urllib.request
import urllib.error

def scrape(url):
    try:
        urllib.request.urlopen(url)
        print(f'URL {url} Scraped')
    except (urllib.error.HTTPError, urllib.error.URLError):
        print(f'URL {url} not Scraped')

if __name__ == '__main__':
    pool = Pool(processes=3)
    urls = [
        'https://www.baidu.com',
        'http://www.meituan.com/',
        'http://blog.csdn.net/',
        'http://xxxyxxx.net'
    ]
    pool.map(scrape, urls)
    pool.close()

##输出结果:
URL https://www.baidu.com Scraped
URL http://xxxyxxx.net not Scraped
URL http://www.meituan.com/ Scraped
URL http://blog.csdn.net/ Scraped

这个例子中我们先定义了一个 scrape 方法,它接收一个参数 url,这里就是请求了一下这个链接,然后输出爬取成功的信息,如果发生错误,则会输出爬取失败的信息。

首先我们要初始化一个 Pool,指定进程数为 3。然后我们声明一个 urls 列表,接着我们调用了 map 方法,第 1 个参数就是进程对应的执行方法,第 2 个参数就是 urls 列表,map 方法会依次将 urls 的每个元素作为 scrape 的参数传递并启动一个新的进程,加到进程池中执行。

这样,我们就可以实现 3 个进程并行运行。不同的进程相互独立地输出了对应的爬取结果。可以看到,我们利用 Pool 的 map 方法非常方便地实现了多进程的执行。

 

 

本篇博客主要来源于  拉钩教育:《52讲轻松搞定网络爬虫》第五讲和第六讲。 对于初步认识多线程和多进程有很大的帮助,尤其是从python的角度,可以很方便的实现。

参考:

第05讲:多路加速,了解多线程基本原理

第06讲:多路加速,了解多进程基本原理

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python快速入门多线程与多进程 的相关文章

  • FastAPI 返回图像

    记录一下 我在网上找了好久都不太管用 from fastapi import FastAPI from fastapi import FastAPI File UploadFile Response from fastapi respons
  • 在 Python 中生成随机字符串

    介绍生成随机字符串的几种方法 1 使用random choice 实现 import string import random number of strings 5 length of string 8 for x in range nu
  • Macbook Pro M1芯片Python开发环境配置

    文章主要介绍M1 Mac新机器如何进行开发环境配置 由于在进行开发环境配置的时候 需要经常使用终端 而且新机器是没有配置homebrew的 后面一些操作不是很方便 所以本文将会对日常终端使用方面进行一些配置 丰富我们的终端样式和使用 文章大
  • Python画图之浪漫樱花

    import turtle as T import random import time 画樱花的躯干 60 t def Tree branch t time sleep 0 0005 if branch gt 3 if 8 lt bran
  • 2021-09-23 opencv学习笔记(图像变换,二值化,滤波器介绍及python实现)

    opencv学习笔记 颜色空间 改变颜色空间 cv2 cvtColor 目标追踪 如何查找某个颜色的HSV值 图形变换 缩放 cv2 resize 平移 旋转 仿射变换 透视变换 二值化 简单阈值法 自适应阈值 Otsu二值化 俗称大津法
  • C 、C++ 、Java、Python、JavaScript数据类型对比介绍

    C C Java Python JavaScript数据类型对比介绍 C C Java Python JavaScript数据类型对比如下 C语言 静态类型语言 需要在声明变量时指定类型 基本数据类型包括 整型 int 通常分为有符号 si
  • python后端学习(十三)路由支持正则、Url编码、增删改操作、增加log日志

    路由支持正则 编码 增删改操作 增加log日志 mini frame py import re url编码相关 import urllib parse import logging from pymysql import connect U
  • python作业记录1_字典运用的实例

    帮朋友做了几个作业题目 记录一下 一 某人到超市购买了以下物品 先需要对货物金额进行统计 清单如下图所示 牛奶 65 面包 15 可乐 39 饼干 45 糖果 24 水果 35 8 要求 1 使用字典保存以上数据 2 可乐的金额统计出错 请
  • 运行python脚本时传入参数的几种方式(接收外部参数)

    运行python脚本时传入参数时三种格式对应不同的参数解析方式 分别为sys argv argparse tf app run 前两者是python自带的功能 后者是tensorflow提供的便捷方式 1 sys argv sys模块是很常
  • Python设置excel单元格格式

    文章目录 xlwt 模块简介 设置数字的格式 设置字体 设置对齐方式 设置边框 设置 填充 设置保护 xlwt 模块简介 xlwt 是 python中一个用来操作 excel 文件的库 其中 封装了很多常用操作 本文主要讲解使用该库在生成e
  • 计算机二级python基础题刷题笔记(三)

    hello 看到三的小伙伴们你们已经超过30 的对手啦 接下来也要加油呀 代码没有最好 只有更好 如果你有更好的想法答案欢迎在评论区里发表呀 1 将程序里定义好的std列表里的姓名和成绩与已经定义好的模板拼成一段话 显示在屏幕里 std 张
  • Python —— 第四部分 面向对象程序设计

    第四部分 面向对象程序设计 4 1 基本概念 4 2 类的定义与使用 4 3 self 参数 4 4 类成员与实例成员 4 4 1 修改属性的值 4 5 成员 4 6 方法 4 7 继承机制 4 7 1 子类的方法 init 4 7 2 给
  • 学完Python,怎么变现?小哥哥10000元外快了解一下

    自学 Python 之后如果不去公司上班 自己一个人可以通过此技能挣什么钱 逆天的Python 只要你掌握了相关技术 就可以靠它赚钱 具体怎么赚 我们来看看一位小哥哥的回答 以我差不多四年的 Python 使用经验来看 大概可以按以下这些路
  • Python学习-----模块1.0(模块的简介、定义与使用)

    目录 前言 1 什么是模块 2 模块的分类 1 内置模块 2 第三方模块 3 自定义模块 3 模块的使用 4 自定义模块 5 模块和执行文件的判断 前言 今天就开始讲Python中的模块篇了 模块是Python的重要组成部分 Python之
  • Python学习-----无序序列1.0(字典的创建、查看、添加、修改、删除/替换)

    目录 前言 字典是什么 字典的特点 1 字典的创建 1 直接创建 2 dict 函数创建 2 字典的查询 1 get 函数 2 获取字典一组内容 3 字典键值对的添加 1 直接添加 2 setdefault 函数 4 字典的修改 updat
  • python3 隐藏print的标准输出

    在使用python调用函数时 想将函数中的print输出禁止掉又不想改函数的代码 可以定义如下HiddenPrints类解决 class HiddenPrints def enter self self original stdout sy
  • python处理excel数据

    文章目录 前言 一 用到的模块是什么 二 execl表格的样式 三 模块的使用 1 引入模块 2 读取excel表数据 3 将写入excel表 四 代码分析 1 代码逻辑 2 选出有用的股票号并与回报率关联 3 将全部数据按照所需要的股票号
  • Mac安装python3

    可以在命令行中输入 brew install python3 但是出现了error 如下所示 tar Error opening archive Failed to open Users my Library Caches Homebrew
  • python自动化课程笔记(六)函数

    函数 类 模块 包 项目 包与文件夹的区别在于 包中有很多模块 和init文件 函数 提高编码的效率及代码的重用 把独立功能的代码块组成一个小模块 def printInfo 定义一个函数 print 10 print 人生苦短 我用pyt
  • Python错误处理的艺术:使用retrying库实现高效重试机制

    简介 学习如何使用 Python 的 retrying 库来处理在程序运行过程中可能出现的各种异常和错误 retrying 是一种简单 易于使用的重试机制 帮助我们处理由网络问题或其他暂时性错误引起的失败 在很多情况下 简单的重试可能就是解

随机推荐

  • 【机器人学】【国家标准】发现《GB/T 29824-2013 工业机器人 用户编程指令》的一个编辑错误

    错误处 近来的主要工作是设计工业机器人控制系统 需要设计一系列的用户编程指令 在查阅国家标准的时候 发现一处错误 封面
  • 探索 Mimikatz 神器之 WDigest

    自从 Mimikatz 出现以来 我们很多人已经对这个工具进行过封装 也尝试过注入 或者是使用 powershell 实现类似的功能 现在我们决定给这个工具添加一个内存 dump 的功能 无论我们如何封装使用 Mimikatz 它仍然是 W
  • Mysql-通过创建存储过程完成“对多个同类表进行查询并将结果汇总储存在新表中”

    案例背景 公司数据储存在N张表中 N是大数 现在需要求每个类别某类数据topN用户的数据 代码结果 创建存储过程将数据从多个相同表中查询求topN并存入 本文主要内容 知识点 分组查询topN问题 https editor csdn net
  • 雅可比矩阵机器学习_深度学习中的Matrix Calculus (一)

    绝大多数网络上对深度学习公式的推导教程notation混乱 而专门介绍Matrix Calculus的材料可能又过于繁杂 其实在深度学习中用到的矩阵微积分并不艰深 看完之后你就可以愉快的进行各种推导了 本文仅作为优秀资源的搬运总结 有余力的
  • SQL 备份表-造数据-还原表

    不影响日后工作的造数据步骤 1 备份表内的内容 2 清表内内容 3 给表造数据 4 还原表 1 备份表内的内容 create table lt 库名2 gt lt 表名2 gt as select from lt 库名1 gt lt 表名1
  • 搭建iscsi存储系统

    内容 常见硬盘介绍 NAS和SAN服务器概述 实战 配置IP SAN服务器 实战 IP SAN服务器日常操作 存储设备 阵列柜 SAS 容量小 300G 600G 900G 价格贵 SATA 容量大 500G 750G 1T 2T 3T 4
  • HackMyVM Influencer

    HackMyVM Influencer 靶机IP 192 168 173 148 信息收集 nmap端口扫描 开启80 2121端口 ftp连接2121端口 匿名用户登录 下载所有文件 查看note txt 依次对图片文件 进行密码爆破 在
  • for和do-while循环以及break和continue语句

    目录 for循环 语法形式 循环的执行流程图 for循环和while循环的区别 例子 do while循环 语法形式 循环的执行流程图 例子 break和continue语句 break和continue在不同循环中的区别 while fo
  • 20张程序员才懂的搞笑图!保准你笑出猪叫......

    温馨提示 请勿在以下场景食用本文 吃饭时 挤电梯时 路过精神病院时 HTML的4种黑法 扫地阿姨 HTML确实是一门编程语言啊 扫地阿姨 所以HTML程序员不能称自己是程序员 扫地阿姨 这个黑的太过分了吧 扫地阿姨 小姑娘我被你的外表欺骗的
  • Java基础之字符常量

    表示一个字符 一个字符常量要用一对英文半角格式的单引号 引起来 它可以是英文字母 数字 标点符号 以及由转义序列来表示的特殊字符 a 1 r u0000 u0000 表示一个空白字符 即在单引号之间没有任何字符 之所以能这样表示是因为 Ja
  • 线程安全集合类概述

    HashTable和Vector 遗留的线程安全实现 效率较低 HashTable map的实现 Vector list的实现 Collections中的带synchronized修饰实现类 如synchronizedMap synchro
  • 反爬机制之验证setcookie

    反爬表现 在使用python或rust进行爬虫的时候的有时会遇到请求返回一段含有只含有js代码的html页面如图 分析及优化文件 首先解决转义字符 x63 x73 x4b x48 x77 x71 x4d x49这种 转义字符可以通过prin
  • 新一配:区块链及其概念【转载】

    区块链是分布式数据存储 点对点传输 共识机制 加密算法等计算机技术的新型应用模式 区块链 Blockchain 是比特币的一个重要概念 它本质上是一个去中心化的数据库 同时作为比特币的底层技术 是一串使用密码学方法相关联产生的数据块 每一个
  • 考研数学-三角函数与反三角函数图像

    转载自 http math001 com inverse trigonometric functions 在三角函数的前面加上 arc 表示它们的反函数 f 1 x 即由一个三角函数值得出当时的角度 1 正弦函数 sin x 反正弦函数 a
  • 泰勒公式浅谈原理(转) ----- 深度好文, 一点是如何蕴含整个世界

    泰勒公式浅谈原理 转 上周写完了 三体 读后思考 泰勒展开 维度打击 黑暗森林 后收到一些邮件 进一步思考了关于泰勒展开的意义 也许我掌握的那些网络技术比如Linux Netfilter NAT之类 太过底层太过小众 所以大家几乎都是没有感
  • [论文阅读] (15)英文SCI论文审稿意见及应对策略学习笔记总结(letpub爬虫)

    娜璋带你读论文 系列主要是督促自己阅读优秀论文及听取学术讲座 并分享给大家 希望您喜欢 由于作者的英文水平和学术能力不高 需要不断提升 所以还请大家批评指正 非常欢迎大家给我留言评论 学术路上期待与您前行 加油 前一篇介绍英文论文实验评估
  • 在 .NET Core 中结合 HttpClientFactory 使用 Polly(下篇)

    译者 王亮作者 Polly 团队原文 http t cn EhZ90oq声明 我翻译技术文章不是逐句翻译的 而是根据我自己的理解来表述的 包括标题 其中可能会去除一些不影响理解但本人实在不知道如何组织的句子 译者序 这是 Polly and
  • c++11中auto&&是什么意思?

    为什么80 的码农都做不了架构师 gt gt gt By using auto var
  • Missing global shader FCopyVelocityGridCS‘s permutation 0, Please make sure cooking was successful.

    打包好的项目报了如下错误 如何解决这个bug呢 这个问题困扰了我很久 从2022年9月左右就无法打包到现在的2023年5月31号 没想到今天解决了 最近我花了一星期的时间来研究这个bug导致 当时以为是蓝图和材质代码导致 一个个查 吧资源的
  • Python快速入门多线程与多进程

    Python快速入门多线程与多进程 多线程 多线程的含义 进程我们可以理解为是一个可以独立运行的程序单位 比如打开一个浏览器 这就开启了一个浏览器进程 打开一个文本编辑器 这就开启了一个文本编辑器进程 但一个进程中是可以同时处理很多事情的