Python学习笔记(十六)多进程和多线程

多进程

要实现多进程(multiprocessing),首先要了解 操作系统的相关知识
Unix/Linux操作系统提供了 一个fork()系统调用,他非常 特殊 ,普通函数调用一次返回一次,fork()调用一次返回两次,这是操作系统自动把当前进程(父进程)复制了一份(子进程),分别在父进程和子进程内返回。
子进程永远返回 0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以父进程要记下没格子进程 的 ID ,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:

import os
print('Process(%s) start...' % os.getpid())
pid = os.fork()
if pid  == 0:
    print('I am child  process  (%s)  and my parent  is  %s' % (os.getpid() , pid))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
应有结果:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.

fork()作用:有了 fork()调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork()出子进程来处理新的http请求。

multiprocessing
上面的代码 Windows上 无法实现
但Python 是跨平台的。multiprocessing模块提供了 一个Process类来代表一个进程对象。
例:启动一个子进程并等待其结束

from multiprocessing import Process
import os
def run_proc(name):
    #子进程要执行的代码
    print('运行子进程%s(%s)......'%(name,os.getpid()))
if __name__ == '__main__':
    print('Run  child process %s(%s)...' % (name, os.getpid()))  #输出父进程
    p = Process(target = run_proc,args=('test',)) #新进程
    print('child  process will start')
    p.start()
    p.join()
    print('child process end')
结果:
父进程5416
子进程将开始
运行子进程test(5420)......
子进程结束

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

from multiprocessing import Pool
import os,  time, random
def long_time_task(name):
    print('Run  task  %s (%s)...' % (name, os.getpid()))
    start = time.time()  #开始时间
    time.sleep(random.random() * 3)
    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)  #限制同时执行的进程数
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))  #参数 是子进程要执行的函数和 子进程名 
    print('Waiting  for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done')
结果:
Parent process  5528.
Waiting  for all subprocesses done...
Run  task  0 (5836)...
Run  task  1 (3400)...
Run  task  2 (5812)...
Run  task  3 (8844)...
Task 0  runs  0.46 seconds
Run  task  4 (5836)...
Task 3  runs  0.58 seconds
Task 2  runs  1.04 seconds
Task 4  runs  0.60 seconds
Task 1  runs  2.74 seconds
All subprocesses done

代码解读 :对pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。
请注意输出的结果,task0,1, 2, 3是立刻执行 的,而 task4要等待前面某个task完成后才执行,这是因为Pool的默认大小在电脑上是4,因此最多同事执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:
p = Pool(5)
就可以同时跑5个进程。
由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
子进程
很多时候,子进程不是自身,而是一个外部进程。创建子进程后,还需要控制子进程的输入和输出。
subprocesses模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
例:在 Python代码中运行nslookup www.python.org,这和在命令行运行结果一样。

import subprocess
print('nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit  code:', r)
结果:
中文字符是乱码
```![在这里插入图片描述](https://img-blog.csdnimg.cn/20200707100612561.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzUyNTIwOQ==,size_16,color_FFFFFF,t_70)
如果子进程还需要输入,则可以通过communicate()方法 输入:

```bash
import subprocess
print('nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit  code:', r)
import subprocess
print('nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE,stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)
output, err = p.communicate(b'set q = mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
结果:中文又是乱码

说是相当于在命令行敲命令:
在这里插入图片描述
进程间通信
Process之间是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装类低层的机制,提供了Queue、Pipes等多种方式来交换数据。
我们以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()
    #写子进程,执行函数write,作用范围是q
    pw  = Process(target=write, args=(q,))
    #读子进程,执行函数read,作用范围是q
    pr  = Process(target=read, args=(q,))
    #启动子进程pw,写入
    pw.start()
    #启动子进程pr,读取:
    pr.start()
    #等待pw结束:
    pw.join()
    #pr进程里是死循环,无法等待其结束,只能强行中止:
    pr.terminate()
结果:
Process  to read: 2580
Pricess to write: 1336
Put A to queue...
Get  A  from  queue
Put B to queue...
Get  B  from  queue
Put C to queue...
Get  C  from  queue

在Unix/Linux下 ,multiprocessing模块封装了 fork()调用,使我们不需要关注fork()的细节 。
因为windo没有fork调用,所以multiprocessing需要“模拟”出fork效果,父进程所有Python对象都必须通过pickle序列化再传到子进程中去,所以,如果multiprocessing在Windows下 调用失败率了 ,要先考虑是不是pickle失败了。

小结
在Unix/Linux下,可以使用fork()调用实现多进程。
要实现跨平台的多进程,可以使用multiprocessing模块。
进程间通信是通过Queue、Pipes等实现的。

多线程

多任务由多进程完成,也可以由一个进程内的多线程完成。
进程是由若干线程组成的,一个进程至少有一个线程。
线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持。
Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading._thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要 使用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) )
        #推迟执行的秒数 1秒
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name )

print('thread %s is running...' % threading.current_thread().name )
#新线程 线程执行函数是loop,线程名是loopThread
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

任何进程默认会启动一个主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数 ,它永远返回当前线程的实例。主线程实例的名字叫MainTread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他 意义,如果不起名字Python就自动给线程命名为Thread-1, Thread-2… …

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(1000000):
        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)
结果:基本每次都不一样

分析:balance是共享变量,由两个线程共享,初值为0,启动两个线程 ,先存后取,理论上结果为0,但线程的语句不是一次就执行完的,并且执行顺序由操作系统决定,语句交替执行,结果就不一样了。
并且高级语言的一条语句在CPU执行时是多条的
比如:balance = balance + n
就是:
1.计算balance + n,存入临时变量中;
x = balance + n
2.将临时变量的值赋给balance。
balance = x

要确保balance计算正确,就要给change_it上锁,当某线程执行时,其它线程不能同时执行,只能等待解锁后才可执行,这样就能避免修改共享变量的冲突。这通过threading.Lock()实现。
给线程执行的方法上把锁,就可以了 ~:

def run_thread(n):
    for i in range(1000000):
        #先要获取锁
        lock.acquire()
        try:
            #更改共享变量
            change_it(n)
        finally:
            #改完了释放
            lock.release()
结果:必为0

获得锁的线程用完后一定要释放锁,否则那些在等待 锁的线程会一直等待下去,所以用try... ...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率大大下降。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
多核CPU
一个死循环会占用一个CPU,多核处理器需要多个死循环才能全部跑满。
死循环:

import threading, multiprocessing
def  loop():
    x = 0
    while True:
        x = x
for i in  range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

Python的线程虽然是真正的线程,但解释器执行代码时会有一个GIL锁:global interpreter lock ,任何 线程执行前,必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到 1个核。
在Python中,可以使用多线程,但不能有效利用多核。除非通过C来扩展但这样就失去了Python简单易用的特点。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁 ,互不影响。
多线程使用参考:https://www.cnblogs.com/guapitomjoy/p/11537612.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值