python入门基础总结笔记(9)——进程和线程

python入门基础总结笔记(9)——进程和线程

学习采用的是廖雪峰老师的python教程,很精彩详细,收获很大,聊表感谢!原文链接:廖雪峰的官方网站

进程和线程?

  • 现代操作系统:比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。
  • 多任务:操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
  • 单核CPU执行多任务时,操作系统轮流让各个任务交替执行**。 多核CPU上并行执行多任务。
  • 什么是进程?
    一个任务就是一个进程,比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

  • 什么是线程?
    有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

目录

  • 多进程
  • 多线程
  • 进程 vs 线程
  • 分布式进程

1.多进程

1.1 multiprocessing模块

要编写多进程的服务程序,Unix/Linux无疑是正确的选择。

如果要在Windows上用Python编写多进程的程序,由于Windows没有fork调用,需要用到Python提供的一个跨平台的多进程支持:multiprocessing模块

multiprocessing模块 提供了一个 Process类 来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))  #获取子进程id

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    #创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()  #启动
    p.join()   #子程序结束后再往下运行
    print('Child process end.')


#结果
Parent process 3220.
Child process will start.
Run child process test (6200)...
Child process end.

注意:其中os.getpid()获取当前进程id , os.getppid()获取父进程id

1.2 Pool(批量创建子进程)

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

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))  #获取当前进程id
    start = time.time()   #返回当前时间的时间戳
    time.sleep(random.random() * 3) #线程推迟指定的时间运行,单位为秒
                                    #random.random()随机生成一个[0,1)之间的随机数
    end = time.time()   #返回任务结束时的时间戳
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))   #得到任务用时

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)   #设置最多同时执行4个进程
    for i in range(5):   #循环0-4
        p.apply_async(long_time_task, args=(i,))#创建子进程时,传入一个执行函数和函数的参数
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()   
    print('All subprocesses done.')

结果:

Parent process 9872.
Waiting for all subprocesses done...
Run task 0 (6212)...
Run task 1 (6240)...
Run task 2 (3336)...
Run task 3 (9020)...
Task 0 runs 0.75 seconds.
Run task 4 (6212)...
Task 3 runs 0.55 seconds.
Task 2 runs 1.75 seconds.
Task 1 runs 2.18 seconds.
Task 4 runs 2.69 seconds.
All subprocesses done.

Pool设置同时最多执行4个进程,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行。也可以同时跑6个进程。

Pool 对象调用 join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close(),调用 close() 之后就不能继续添加新的 Process 了。

1.3 进程间通信

Python的 multiprocessing模块 包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

结果:

Process to write: 7152
Put A to queue...
Process to read: 2328
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

由于 Windows 没有 fork 调用,因此,multiprocessing 需要“模拟”出 fork 的效果。

父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

2.多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成,一个进程至少有一个线程。

高级语言通常都内置多线程的支持,Python也不例外,Python的标准库提供了threading高级模块

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

import time, threading

# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))   #返回当前线程实例名
        time.sleep(1)   #延时
    print('thread %s ended.' % threading.current_thread().name)


print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

结果:

thread MainThread is running...    #主线程
thread LoopThread is running...    #子线程
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

由于任何进程默认就会启动一个线程,我们把该线程称为主线程。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。

current_thread()函数:它永远返回当前线程的实例。

2.1 Lock

多线程和多进程最大的不同:

多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响。

多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,

因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

import time, threading

# 假定这是你的银行存款:
balance = 0   #定义变量

def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(2000000):
        change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

#结果
-8

以上启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了

如果我们要确保balance计算正确,就要给change_it()上一把锁

balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()

该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。

2.2 多核CPU

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

Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。

3.进程vs线程

3.1 多进程和多线程优缺点

多进程和多线程是实现多任务最常用的两种方式。两种方式的优缺点如下:

多进程

  • 优点:稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了)
  • 缺点:创建进程的代价大;另外,在内存和CPU的限制下操作系统能同时运行的进程数也是有限的。

多线程

  • 优点: 多线程的效率比多进程要高一些。
  • 缺点:任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

无论是多进程还是多线程,只要数量一多,效率肯定上不去,因为多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

3.2 计算密集型 vs.IO密集型

  • 计算密集型

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。

这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写

  • IO密集型

IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。

对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

3.3 异步IO

考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。

现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型

对应到Python语言,单线程的异步编程模型称为协程

4.分布式进程

在Thread(多线程)和Process(多进程)中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。

Python的 multiprocessing模块 不但支持多进程,其中managers子模块 还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

具体参见:分布式进程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值