python学习笔记分享(三十九)网络爬虫(6)进程,线程简介

IT Xiao Ang Zai     2019年4月3号

版本:python3.7

编程软件:Pycharm,Sublime Text 3

一:并发与并行

1.定义:并发是指在一个时间段内发生若干事件,并行是指在同一时刻发生若干事件。

2.例子:单核CPU中,多个任务是以并发的方式运行的,各个任务分别会占用CPU的一段时间依次运行,如果任务在分得的时间内没有完成任务,就会切换到另一个任务,在下次得到CPU使用权的时候再继续执行,直到完成。由于每次运行时间很短,又经常切换,就给了我们错觉是再"同时进行"。而使用多核CPU时,多个任务是以并行方式运行的,各个核的人物能够同时运行,实现了真正的同时运行。

单核CPU执行多任务是以并发的方式执行的。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

二:同步与异步

同步就是并发或并行的各个任务不是独自运行的,人物之间有交替顺序。异步就是并发或并行的各个任务可以单独运行,互不影响。

三:进程与线程介绍

1.简介:对于操作系统来说,一个任务就是一个进程。而在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程。由于每个进程至少要干一件事,所以,一个进程至少有一个线程。

更详细地理解为:

(1)线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。

(2)一个程序的执行实例就是一个进程。每一个进程提供执行程序所需的所有资源。(进程本质上是资源的集合)

一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程。

每一个进程启动时都会最先产生一个线程,即主线程。然后主线程会再创建其他的子线程。

2.如何执行多任务

一种是启动多个进程,每个进程只有一个线程,但多个进程可以一块执行多个任务。

另一种方法是启动一个进程,在一个进程内启动多个线程,多个线程也可以一块执行多个任务。

三种方法是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,这种模型复杂,很少采用。

启动多个任务,有时候任务之间需要通信和联系,有时还要协调好顺序,因此多进程或多线程的程序复杂程度要高很多。但有时不得不这样做,或者用于节省时间,就需要进行多进程或多线程,或两者都进行的编程。

3.进程与线程关系

线程是最小的执行单元,而进程由至少一个线程组成。调度进程和线程,完全由操作系统决定,程序不能决定什么时候执行,执行多长时间。

4.区别

(1)同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
(2)同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
(3)对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
(4)线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
(5)同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
(6)创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
(7)一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
(8)线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。

四:多进程编程

1.由于Windows系统没有fork调用,因此无法像Mac系统下进行fork调用。

但python是一个跨平台的语言,自然也应该有一个跨平台的多进程支持。就是用multiprocessing模块。

multiprocessing模块提供了一个Process类来代表一个进程对象。

2.启动子进程

下面是一个例子演示了启动一个进程并等待结束:

from multiprocessing import Process
import time
import os

#子进程需要执行的代码
def secondPro(name):
    print("这是第{num}个进程".format(num=name))

if __name__ == '__main__':
    #    getpid函数用来取得目前进程的进程ID,
    #    许多程序利用取到的此值来建立临时文件,
    #    以避免临时文件相同带来的问题
    print("第一个进程的识别码为:%s" % os.getpid())
    second = Process(target=secondPro,args=('二',))
    print("子进程开始")
    second.start()
    second.join()
    print("子进程结束")

结果如下:

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,而join()方法可以等待子进程结束后再继续往下运行,确保子进程先进行。

3.启动大量进程

如果要启动大量进程,可以用进程池的方式批量创建子进程。

from multiprocessing import Pool
import time
import os

#子进程需要执行的代码
def everyPro(name):
    start = time.time()
    print("这是第{num}个进程".format(num=name))
    print("第{a}个进程的识别码为:{b}".format(a=name,b=os.getpid()))
    time.sleep(3)
    end = time.time()
    print("第 %s 个进程运行的时间为 %s 秒" % (name,end-start))

if __name__ == '__main__':
    #    getpid函数用来取得目前进程的进程ID,
    #    许多程序利用取到的此值来建立临时文件,
    #    以避免临时文件相同带来的问题
    print("第一个进程的识别码为:%s" % os.getpid())
    #调用进程池
    p = Pool(5)
    print("调用5个进程")
    for i in range(1,6):
        p.apply_async(everyPro,args=(i,))   #调用子进程创建函数
    print("等待所有的进程结束")
    p.close()
    p.join()
    print("进程结束")

结果如下:

这里用先生成一个Pool对象,然后用apply_async()函数进行进程函数调用与传参,根据函数名可以知道该函数是异步的。对Pool对象调用join()方法会等待所有子进程执行完毕调用join()之前必须先调用close(),进行进程的关闭,调用close()之后就不能继续添加新的Process了,如果设置Pool的参数,就可以设置最多同时执行多少个进程,看到先把前面的进程执行完毕后,才能够执行下面的进程,默认大小是CPU的核数。

代码如下:

from multiprocessing import Pool
import time
import os

#子进程需要执行的代码
def everyPro(name):
    start = time.time()
    print("这是第{num}个进程".format(num=name))
    print("第{a}个进程的识别码为:{b}".format(a=name,b=os.getpid()))
    time.sleep(3)
    end = time.time()
    print("第 %s 个进程运行的时间为 %s 秒" % (name,end-start))

if __name__ == '__main__':
    #    getpid函数用来取得目前进程的进程ID,
    #    许多程序利用取到的此值来建立临时文件,
    #    以避免临时文件相同带来的问题
    print("第一个进程的识别码为:%s" % os.getpid())
    #调用进程池
    p = Pool(4)
    print("调用5个进程")
    for i in range(1,6):
        p.apply_async(everyPro,args=(i,))   #调用子进程创建函数
    print("等待所有的进程结束")
    p.close()
    p.join()
    print("进程结束")

结果如下:

可以看到第5个进程必须等到前面的进程执行完毕后才能够执行。

4.外部进程详解

(1)有时候,子进程并不是本身,而是一个外部进程。因此有时还需要进行输入输出操作。

subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。

(2)下面是该模块的一些函数用法:
subprocess.run()
subprocess.call()
subprocess.Popen()

run()方法
subprocess.run(['df', '-h'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
stdout标准输出,stdin标准输入,stderr标准错误
subprocess.PIPE是把结果存到管道里进行缓存

call()方法
a = subprocess.call(['df', '-h'])
执行命令,返回命令执行状态,0 或 非0

args:shell命令,可以是字符串或者序列类型(如:list,元组)
stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
shell:同上
cwd:用于设置子进程的当前目录
env:用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。

还有一个常用方法:

communicate()       和启动的进程交互,通过stdin发送数据,通过stdout接收输出数据    只能交互一次

还有一些其他方法,这里就不一一介绍了,后面如果有实例,再配合讲解。

5.进程间通信

操作系统提供了很多机制来实现进程间的通信,Python的multiprocessing模块,提供了QueuePipes等多种方式来交换数据。父进程所有Python对象都通过pickle序列化打包再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是打包失败了。当年关系有点多,且比较难懂,就不详讲了,之后用到时再一一解释。

五:多线程编程

1.多线程简介:多任务也可以用一个进程的多线程来完成。由于线程是操作系统直接支持的,很多语言都支持多线程。

python标准库提供了两个模块进行多线程编程。这两个模块是_thread和threading,第二个比较高级一些,因此大多数用第二个模块。

2.启动多线程:

下面有两种方法启动多线程:
第一个方法就是把一个函数传入并创建Thread实例,然后调用start()开始执行。

代码如下:

import time,threading

#新线程执行的代码
def myNewloop():
    print("线程的名字为:%s" % threading.current_thread().name)

if __name__ == '__main__':
    print("线程的名字为:%s" % threading.current_thread().name)
    for i in range(5):
        loop = threading.Thread(target=myNewloop, name=i)
        loop.start()
        loop.join()
    print("线程{0}结束".format(threading.current_thread().name))

结果如下:

任何进程默认就会启动一个线程,称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。如果不起名字Python就自动给线程命名为Thread-1Thread-2。

3.LOCK

(1)介绍

互斥锁

再多进程中,各个变量互不影响。而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,就会造成数据混乱。因此,我们必须确保一个线程在修改一个变量的时候,别的线程一定不能改。我们可以给线程函数上一把锁,当某个线程执行该函数时,就可以说,该线程因此获得了锁,其他线程不能同时执行该函数,只能等待,直到锁被释放后,才能够更改,同时刻只能有一把锁。

(2)创建一个锁就是通过threading.Lock()来实现,然后用lock.acquire()。当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。当锁用完后一定要释放,我们用try...finally来确保锁一定会被释放

(3)锁的优点和缺点:
优点:某段关键代码或变量只能由一个线程执行。

缺点:阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降。

当某个线程想要获取其他线程的锁时,可能会造成死锁,只能强制关闭程序。

4.多核CPU

再python中,启动N个线程,最大不能把CPU利用完毕。这是因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。只能并发利用CPU

不过可以通过多进程实现多核任务。

因此再多线程中只能够并发,却不能够并行。

5.ThreadLocal

很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见。因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问。这里就出现了hreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。解这样就解决了参数在一个线程中各个函数之间互相传递的问题。可以简化代码。最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

猜你喜欢

转载自blog.csdn.net/ITxiaoangzai/article/details/88982707