Python 中的多进程和多线程

Python 中的多进程和多线程

多进程多线程是实现多任务及提升任务执行效率的两种常用方法。两者都遵循 Master-Worker 模式,Master 负责分配任务,Worker 负责执行任务。具体来说,多进程由一个主进程与多个子进程构成,而多线程由一个主线程与多个子线程构成。两者的一个主要区别在于:多进程的每个进程创建在不同的内存空间中,而多线程创建在一个进程中。

一、 多进程与多线程的优缺点对比:

  • 多进程的优点:稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程(当然主进程崩溃了所有进程就全崩溃了,但是主进程只负责分配任务,崩溃的概率很低)。多进程模式的另一个优点是可以将进程分布到多台机器上。
  • 多进程的缺点:创建进程的代价大,一般情况下创建的进程数不超过CPU核数的2倍。
  • 多线程的优点:系统开销较小,在Windows下,多线程的效率比多进程要高
  • 多线程的缺点:稳定性差,由于多线程创建在一个进程中,这就意味着多个线程共用相同的内存空间,所以当一个线程崩溃,其它线程也会崩溃。此外,多线程不能分布在多台机器上。
  • Python解释器由于设计时有GIL全局锁,导致无法利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

二、 多进程的实现

1. 创建进程

通过调用 multiprocessing 模块的 Process 可以创建一个进程对象,创建进程之后,操作系统会自动把当前进程(称为父进程)复制一份(称为子进程),然后,分别在父进程和子进程内返回,子进程永远返回0,而父进程返回子进程的ID。os 模块的 getpid() 和 getppid() 分别用于获取当前进程及当前进程父进程的 ID

from multiprocessing import Process
import os

def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))
    print('Run parent process %s (%s)...' % (name, os.getppid()))

if __name__ == '__main__':
	print('Parent process %s.' % os.getpid())
	p = Process(target = run_proc, args = ('test',))
	p.start()  # 进程开始
	p.join()   # 等待结束
	print('End')
2. 创建进程池

通过调用 Pool() 模块创建一个进程池,对进程池调用 apply_async() 可以创建多个进程。如果创建的进程数超过 CPU 核数,则多出的排在后面的进程会等到前面的进程执行完毕之后才执行。

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)    # 进程池的个数一般取CPU个数的1到2倍
    for i in range(8):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()  # close 后不能再添加进程
    p.join()
    print('All subprocesses done.')
3. 进程间共享变量
from multiprocessing import Process, Value, Array  #引入多进程版本的value和array
def f(n, a):
    n.value = 3.1415927
    for i in range(len(a)):
        a[i] = -a[i]

if __name__ == '__main__':
    num = Value('d', 0.0)
    arr = Array('i', range(10))
    p = Process(target=f, args=(num, arr))
    p.start()
    p.join()
    print(num.value)
    print(arr[:])
4. 进程间通信

通过在进程间引入 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__':
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    pw.start()
    pr.start()
    pw.join()
    pr.terminate()  # pr 是死循环,需要强行停止
5. 创建并控制子进程
import subprocess

# 下面的代码相当于直接执行命令`nslookup www.python.org`
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

# 如果想要输入,需要调用 communicate()
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)
print('Exit code:', p.returncode)

三、多线程的实现

1. 创建线程

每调用一次 threading.Thread() 一个线程,可以通过 threading.Thread()中的 name 属性为每个线程命名。

def loop():
    thread_name = threading.current_thread().name  # 获取当前线程的名称
    print('Thread %s is running...' % thread_name)
    n = 0
    while n < 5:
        n = n + 1
        print('Thread %s >>> %d' % (thread_name, n))
    print('Thread %s ends.' % thread_name)
    
thread_name = threading.current_thread().name
print('Thread %s is running...' % thread_name)
t1 = threading.Thread(target = loop, name='LoopThread-1')  # 为线程命名,t1执行完之后才会执行t2
t2 = threading.Thread(target = loop, name='LoopThread-2') 
t1.start()
t2.start()
t1.join()
t2.join()
print('Thread %s ends.' % thread_name)
2. 在多线程中添加线程锁

多进程中,同一个变量各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改。如何各线程正常执行一般不会有什么问题,但是如果某个线程意外中断,就会导致变量被赋予意想不到的数值,为了防止这种情况发生就需要添加线程锁。线程锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,比如它阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。

import time, threading

local_school = threading.local() # 定义threadinglocal对象

def process_student(std):
    std = local_school.student  # 获取当前线程的局部变量
    print('Hello %s (%s)\n' % (std, threading.current_thread().name))

def process_thread(name):
    local_school.student = name # 将name传入threadinglocal对象
    process_student(name)

t1 = threading.Thread(target = process_thread, args = ('Tom', ), name = 'TA')
t2 = threading.Thread(target = process_thread, args = ('Jack', ), name = 'TB')
print(t1.name)
print(t2.name)
t1.start()
t2.start()
t1.join()
t2.join()
3. 线程局部变量

为每个线程赋予专属的局部变量可以让各线程独立运行。实现逻辑为,先定义一个线程局部变量,这个变量会与创建的线程绑定在一起,然后在线程中调用函数时,被调用的函数会通过线程名称获取到这个局部变量。

import time, threading

local_school = threading.local() # 定义threadinglocal对象

def process_student():
    std = local_school.student  # 获取当前线程的局部变量
    print('Hello %s (%s)\n' % (std, threading.current_thread().name))

def process_thread(name):
    local_school.student = name # 将name传入threadinglocal对象
    process_student()

t1 = threading.Thread(target = process_thread, args = ('Tom', ), name = 'TA')
t2 = threading.Thread(target = process_thread, args = ('Jack', ), name = 'TB')
t1.start()
t2.start()
t1.join()
t2.join()

ThreadLocal 最常用的地方就是为每个线程绑定一个数据库连接,HTTP 请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

4. 线程池
import time
import threadpool

def long_op(n):
    print('%d\n' % n)
    time.sleep(2)

pool = threadpool.ThreadPool(2)
tasks = threadpool.makeRequests(long_op, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(len(tasks))
[pool.putRequest(task) for task in tasks]
pool.wait()

猜你喜欢

转载自blog.csdn.net/weixin_42902669/article/details/85203595