Python-100days-13

Day13 - 进程和线程

同时执行多任务就是并发编程,并发编程有两个重要的概念,一个叫进程,一个叫线程。

进程与线程

进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据线以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。进程可以通过fork(派生)或spawn的方式创建新的进程来执行其他任务,不过新的进程也有自己独立的内存空间,因此必须通过进程间通信机制来实现数据共享,具体方式包括管道、信号、套接字、共享内存区等。
一个进程可以拥有多个并发的执行线索,简单来说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当在单核CPU系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的主要唯一的一个线程,多个线程共享CPU的执行时间。使用多线程实现并发编程为程序带来的好处不言而喻,最主要的体现在提升程序的性能和改善用户体验。

ps.进程就是系统中的一个程序,每个程序都是一个进程。进程可以创建进程,但是它们必须通过IPC进行进程通信。每个进程可以有多个线程,由于都是一个爹生的(同一个进程),所以线程间交流很容易。多核CPU才能实现真正意义上的并发编程,如果只有单个CPU,那么只能实现伪多线程,CPU在多个线程任务之间快速切换,假装是多线程运行。

多线程有好处也有坏处,占用很多的CPU资源,并且开发稳定运行的多线程程序难度较大。
Python实现并发编程主要有三种方式:多进程、多线程、多进程+多线程

Python中的多进程

Unix和Linux操作系统上提供了fork()系统调用来创建进程,调用fork()函数的是父进程,创建出的是子进程,子进程是父进程的一个拷贝,但是子进程拥有自己的PID。fork()函数非常特殊它会返回两次,父进程中可以通过fork()函数的返回值得到子进程的PID,而子进程中的返回值永远都是0。Python的os模块提供了fork()函数。由于Windows系统没有fork()调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块的Process类来创建子进程,而且该模块还提供了更高级的封装,例如批量启动进程的进程池(Pool)、用于进程间通信的队列(Queue)和管道(Pipe)等。

ps.Python中os模块提供fork函数。实现跨平台并发编程需要multiprocessing中的process类创建子进程。

敲个例子,使用多进程的方式将两个下载任务放到不同进程中。

from multiprocessing import Process
from os import getpid
from random import randint
from time import time, sleep

def download_task(filename):
    print('启动下载进程,进程号[%d].' % getpid())
    print('开始下载%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))

def main():
    start = time()
    p1 = Process(target=download_task, args=('Python从入门到住院.pdf', ))
    p1.start()
    p2 = Process(target=download_task, args=('Peking Hot.avi', ))
    p2.start()
    p1.join()
    p2.join()
    end = time()
    print('总共耗费了%.2f秒.' % (end - start))

if __name__ == '__main__':
    main()

通过Process类创建进程对象,target传入进程启动后要执行的代码,args是一个元组,代表要传递给函数的参数。Process对象通过start启动进程,join方法表示等待进程执行结束。

多个子进程中进行通信

Python的标准库中有一个Queue库,multiprocessing中也有一个Queue库。前者当get队列为空时,会卡死。这部分需要多看几遍。

Python中的多线程

目前多线程开发推荐threading模块,它提供更好的面向对象的封装。

from random import randint
from threading import Thread
from time import time, sleep

def download(filename):
    print('开始下载%s...' % filename)
    time_to_download = randint(5, 10)
    sleep(time_to_download)
    print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))

def main():
    start = time()
    t1 = Thread(target=download, args=('Python从入门到住院.pdf',))
    t1.start()
    t2 = Thread(target=download, args=('Peking Hot.avi',))
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('总共耗费了%.3f秒' % (end - start))

if __name__ == '__main__':
    main()

我们可以通过threading模块的Thread类创建线程,还可以通过继承Thread类创建自定义的线程类,再创建线程对象并启动线程。

from random import randint
from threading import Thread
from time import time, sleep

class DownloadTask(Thread):

    def __init__(self, filename):
        super().__init__()
        self._filename = filename

    def run(self):
        print('开始下载%s...' % self._filename)
        time_to_download = randint(5, 10)
        sleep(time_to_download)
        print('%s下载完成! 耗费了%d秒' % (self._filename, time_to_download))

def main():
    start = time()
    t1 = DownloadTask('Python从入门到住院.pdf')
    t1.start()
    t2 = DownloadTask('Peking Hot.avi')
    t2.start()
    t1.join()
    t2.join()
    end = time()
    print('总共耗费了%.2f秒.' % (end - start))

if __name__ == '__main__':
    main()

因为多个线程可以共享进程的内存空间,因此要实现多个线程间的通信相对简单,大家能想到的最直接的办法就是设置一个全局变量,多个线程共享这个全局变量即可。但是当多个线程共享同一个变量(我们通常称之为“资源”)的时候,很有可能产生不可控的结果从而导致程序失效甚至崩溃。如果一个资源被多个线程竞争使用,那么我们通常称之为“临界资源”,对“临界资源”的访问需要加上保护,否则资源会处于“混乱”的状态。
ps.Python多线程并不能发挥CPU多核特性,这是由于Python解释器有一个全局解释器锁GIL,任何线程执行前都必须先获得GIL锁,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有集合执行,这是一个历史遗留问题,但是即便如此,使用多线程在提升效率和改善用户体验方面仍然有积极意义。

多进程与多线程应用

无论是多进程还是多线程,当数量超过一定量时,效率必然降低。道理很简单,每次切换任务都需要耗时耗资源。
是否采用多任务要看任务的类型。当任务属于计算密集型任务时,这种任务全靠CPU运算,这类任务主要消耗CPU资源,用Python执行效率通常很低,最好是用C语言实现。
涉及到网络、存储介质I/O的任务可视为I/O密集型任务,这类任务特点是CPU消耗少,大部分时间都在等I/O操作完成。对于此类任务适合启用多任务执行,能让CPU高效运转。

单线程+异步I/O

现代操作系统对I/O操作的改进中最为重要的就是支持异步I/O。如果充分利用操作系统提供的异步I/O支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。Nginx就是支持异步I/O的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。用Node.js开发的服务器端程序也使用了这种工作模式,这也是当下实现多任务编程的一种趋势。
在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

关于练习

ps.还在看这方面的资料,先不着急敲练习示例,等看完了在敲吧,偷个懒hhh

你可能感兴趣的:(Python-100days-13)