10-Python进程与线程

Python进程

创建新进程

from multiprocessing import Process
import time
def run_proc(name):       #子进程要执行的代码
    for i in range(5):      #每秒输出一个name
        print(name)
        time.sleep(1)
if __name__ == '__main__':
    p = Process(target=run_proc, args=('test',))  #创建进程
    p.start()      #开始进程
    print('Child process end.')

解释:

  • 在Python中,通过函数来定义一个进程
  • target指定函数名,即要运行的进程
  • args指定函数参数
  • 主进程运行完后,子进程并不会结束。
  • 创建子进程需要在main中进行

守护进程

守护进程:父进程退出后,子进程也退出。

from multiprocessing import Process
import time
def run_proc(name):       #子进程要执行的代码
    for i in range(5): #每秒输出一个name
        print(name)
        time.sleep(1)
if __name__ == '__main__':
    p = Process(target=run_proc, args=('test',))  #创建进程
    p.daemon = True #设置为守护进程
    p.start()      #开始进程
    time.sleep(3)
    print('Child process end.')

结果:

test
test
test
Child process end

解释:

  • 只输出了3次test,而不是5次。
  • 因为3s后,主进程退出;主进程结束后,子进程也结束。
  • p.daemon=True需要在start()之前运行。(一旦开始运行,就来不及设置为守护进程了)

join()方法

join():暂停运行当前进程,直到子进程运行完。

from multiprocessing import Process
import time
def run_proc(name):       #子进程要执行的代码
    for i in range(5): #每秒输出一个name
        print(name)
        time.sleep(1)
if __name__ == '__main__':
    p = Process(target=run_proc, args=('test',))  #创建进程
    p.start()      #开始进程
    p.join()        #等待子进程运行完
    print('Child process end.')

结果:

test
test
test
test
test
Child process end.

解释:

  • 一旦调用join(),主进程就暂停运行。
  • 等待p进程运行完,主进程继续运行。

terminate()方法

terminate():立即停止该进程

from multiprocessing import Process
import time
def run_proc(name):       #子进程要执行的代码
    for i in range(5): #每秒输出一个name
        print(name)
        time.sleep(1)
if __name__ == '__main__':
    p = Process(target=run_proc, args=('test',))  #创建进程
    p.start()      #开始进程
    time.sleep(2)
    p.terminate()   #2s后停止运行p进程
    print('Child process end.')

进程池

进程池可以同时管理多个子进程

from multiprocessing import Pool
def run_proc(name):       #子进程要执行的代码
    print(name)
if __name__ == '__main__':
    p = Pool(4)       #最多同时执行4个进程(一般为CPU核数),有进程运行完腾出的空间再分配给其他进程运行
    for i in range(5):
        p.apply_async(run_proc, args=(i,)) #在进程池中添加进程
    p.close()     #执行join()前必须执行close(),表示不能继续添加新的Process了
    p.join()      #等待子进程结束再往下执行

解释:

例子中使用了join方法,等待进程中的子进程运行完,再接着往下运行,如果子进程陷入死循环,那主进程就会一直等待。
使用p.terminate()方法,结束进程池中的所有进程,可以防止子进程成为僵尸进程。

p = Pool(4)表示最多同时执行4个进程
若往进程池中添加第5个进程(假设此时前4个进程没有运行完) ,那第5个进程不会立即运行。前4个进程中,其中一个进程运行完后,会腾出一个位置,第5个进程替补,所以最多同时运行4个进程

subprocess

subprocess使得子进程可以调用另一个程序,并提供输入输出操作。

例1:

import subprocess
ret = subprocess.call(['ping', 'www.python.org'])  #执行命令,返回状态码
print('Exit code:', ret)   #输出状态码

结果:

正在 Ping dualstack.python.map.fastly.net [151.101.76.223] 具有 32 字节的数据:
来自 151.101.76.223 的回复: 字节=32 时间=102ms TTL=48
来自 151.101.76.223 的回复: 字节=32 时间=106ms TTL=49
来自 151.101.76.223 的回复: 字节=32 时间=105ms TTL=49
来自 151.101.76.223 的回复: 字节=32 时间=104ms TTL=48

151.101.76.223 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 102ms,最长 = 106ms,平均 = 104ms
Exit code: 0

例2-在子进程中交互式输入:

import subprocess
#执行命令
p = subprocess.Popen(['python'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#输入
output, err = p.communicate(b'print("hello")\nquit()') #输入print "hello"和quit()
print(output.decode('utf-8'))
print('Exit code:', p.returncode)  #输出退出码

结果:

hello

Exit code: 0

解释:

  • 如果子进程运行错误,错误将会保存在err中

进程间通信

生产者和消费者:

from multiprocessing import Process, Queue
import os, time, random
def write(q):        #写进程
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)   #写入value
        time.sleep(random.random())
def read(q):         #读进程
    while True:
        value = q.get(True)       #读出value
        print('Get %s from queue.' % value)
if __name__ == '__main__':
    q = Queue()             #创建Queue
    pw = Process(target=write, args=(q,))  #创建写进程,传入队列q
    pr = Process(target=read, args=(q,))   #创建读进程,写入队列q
    pw.start()
    pr.start()
    pw.join()
    pr.terminate()   #pr进程里是死循环,无法等待其结束,只能强行终止

解释:

  • Queue队列可以用来进程间通信。
  • put函数:往队列里写内容。
  • get函数:读出队列中的元素。

结果:

Put A to queue...
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

Python多线程

  • 进程运行在操作系统中,由操作系统进行管理。
  • 线程运行在Python虚拟机(解释器)中,由Python进行管理。
  • 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行,保证共享内存的数据正确。
  • 所以Python多线程是假的多线程,同一时间,只有一个线程在运行,并不能有效利用CPU多核。
  • 综上,Python多线程适合IO密集型操作,如爬虫(时间都浪费在了等待Response)。尽量避免使用Python做CPU密集型操作。

创建多线程

import threading
def func1(name):
    for i in range(5):
        print(name,i)
        t1=threading.Thread(target=func1,args=("t1",))    #创建线程,执行函数func1,参数为"t1",注意逗号
t1.start()    #启动线程
t1.join()     #等待t1线程执行完

threading中其他函数:

threading.current_thread()  #返回当前线程信息
threading.active_count()    #返回当前活动的线程数(未运行完的线程)
t1.setDaemon(True)          #将t设为守护线程(在线程start()之前),主线程结束后守护线程不再运行

线程之间共享数据

示例:

import threading
a=0
def func1():
    global a
    for i in range(500):
        a+=1
        t1=threading.Thread(target=func1)
t2=threading.Thread(target=func1)
t1.start()    #启动线程
t2.start()    #启动线程
t1.join()     #等待t1线程执行完
t2.join()     #等待t2线程执行完
print(a)

结果:

1000

解释:

  • global声明了a是全局变量,而不是局部变量。
  • 但这种方式是不安全的。

为什么不安全:

import threading
balance = 0
def run_thread(n):
    for i in range(1000000):
        global balance
        balance = balance + n       #注意这里!!!
        balance = balance - 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)      #结果很可能不是0

多线程-锁

给共享变量上一把锁,保证数据的正确,解决上例中的问题。

import threading
balance = 0
lock = threading.Lock()     #创建一个锁
def run_thread(n):
    for i in range(100000):
        lock.acquire()      #先要获取锁
        try:    #放心地改吧
            global balance
            balance = balance + n
            balance = balance - n
        finally:
            lock.release()  #改完了一定要释放锁
            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)      #结果为0

Python协程

进程:由操作系统管理。
线程:由Python虚拟机(解释器)管理。
协程:由程序本身管理,所以开销比线程更小。

协程的定义:

协程,又称微线程纤程。英文名Coroutine

协程的优点:

不是真正的并发,所以共享数据无需加锁。
由程序本身控制,代码间的切换开销极小。

协程的缺点:

无法利用多核资源。因为协程的本质是单线程,只在单个核上运行。但可以与进程配合解决这个问题。

协程的原理:

协程通过yield实现,yield可以模拟程序的中断,进而切换到另一个程序。
协程并不是真正的并发,而是通过yield模拟中断去执行另一个函数。

在Python中使用协程并不需要我们自己使用yield,调用模块即可

greenlet模块

greenlet是一个用C实现的协程模块。
通过设置.switch()可以实现任意函数之间的切换。
缺点:这种切换属于手动切换,当遇到IO操作时,程序会阻塞,而不能自动进行切换。

安装:

pip install greenlet

示例:

from greenlet import greenlet
import time
def test1():
    print("running test1")
    gr2.switch()    #切换到test2
    print("running test1 again ")
    time.sleep(2)
    gr2.switch()
def test2():
    print("running test2")
    gr1.switch()    #切换到test1
    print("running test2 again")
    gr1 = greenlet(test1)  #实例化一个协程
gr2 = greenlet(test2)  #实例化另一个协程
gr1.switch()   #执行gr1

gevent模块

Gevent遇到IO操作时,会进行自动切换,属于主动式切换。

安装:

pip install gevent

示例:

import gevent, time
def func1():
    print('主人来电话啦...')
    gevent.sleep(3)
    print(' 主人那家伙又来电话啦...')
def func2():
    print('打个电话...')
    gevent.sleep(2)
    print('咦,电话给挂了,接着打...')
def func3():
    print("哈哈哈哈")
    gevent.sleep(0)
    print("嘿嘿嘿....")
    start_time = time.time()
gevent.joinall([
    gevent.spawn(func2),  #生成一个协程
    gevent.spawn(func1),
    gevent.spawn(func3),
    ])
print("running time:", (time.time() - start_time))

结果:

打个电话...
主人来电话啦...
哈哈哈哈
嘿嘿嘿....
咦,电话给挂了,接着打...
主人那家伙又来电话啦...
running time: 3.0478341579437256

解释:

可以看出gevent模块切换的效果,遇到IO操作时会自动进行切换,会不停的进行循环遍历,直到切换到已经完成IO操作的协程上去。


posted @ 2020-07-12 23:13  NetRookieX  阅读(16)  评论(0编辑  收藏  举报