Python多进程和多线程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python多进程和多线程相关的知识,希望对你有一定的参考价值。

多进程

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

想要在windows平台编写多进程,需要引入multiprocessing模块。

from multiprocessing import Process
import os

def run_proc(name):
    print(child pid %s %s /but parent is %s % (name, os.getpid(), os.getppid()))
    #fork出的子进程通过getppid()函数可以拿到父进程id

if __name__ == __main__:
    print(parent pid is %s % os.getpid())
    p = Process(target=run_proc, args=(test,)) #multiprocessing模块提供了一个Process类来代表一个进程对象,在这里实例化这个类,传入执行函数及函数参数
    p.start() #启动子进程实例
    p.join() #join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
    print(child end)

程序结果:

parent pid is 4736
child pid test 5316 /but parent is 4736
child end

 join()函数的意思就是,在子进程和主进程做到同时结束

如果注释掉p.join()这一行代码

输出如下:

parent pid is 8152
child end
child pid test 2808 /but parent is 8152

子进程提前结束了,就导致了不同步。

 

进程池方式可以大量创建子进程

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()   #close()函数是指进程池添加完后不允许新的进程加入
    p.join()    
    print(All subprocesses done.)  #Pool(4)代表当前cpu为4核心,所以能同时进行4个子进程 多出的一个task4处于等待

执行结果:

Parent process 5460.
Waiting for all subprocesses done...
Run task 0 (9544)...
Run task 1 (7808)...
Run task 2 (8176)...
Run task 3 (4336)...
Task 3 runs 1.67 seconds.
Run task 4 (4336)...
Task 0 runs 1.95 seconds.
Task 2 runs 1.95 seconds.
Task 4 runs 0.31 seconds.
Task 1 runs 2.52 seconds.
All subprocesses done.

apply_async()异步函数,传入执行函数和函数参数。使每一个子进程不在同一时刻将子进程交给cpu。

 

进程间的通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

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

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

#写数据进程执行的代码:
def write(q):
    print(Process to write: %s parent pid is : %s % (os.getppid(), 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  parent pid is : %s  % (os.getpid(), os.getppid()))
    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.start()  # 启动子进程pw,写入:
    pr.start()   # 启动子进程pr,读取:
    pw.join()  # 等待pw结束:
    pr.terminate()  # pr进程里是死循环,无法等待其结束,只能强行终止:

执行结果:

Process to write: 6264 parent pid is : 1740
Put A to queue...
Process to read: 5296 parent pid is : 1740
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

 

 

多线程

Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

import threading
import time
import os

# 新线程执行的代码:
def loop():
    print(thread %s is running... % threading.current_thread().name)  # threading.current_thread()函数可以拿到当前执行线程的名称
    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(Main pid is %s % os.getpid())
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)

执行结果:

Main pid is 8380
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.

 线程锁LOCk

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

我的理解是,每个进程都会申请属于自己的内存,而线程存在的进程只会申请一次内存、只要存在于同一个进程内的所有线程变量都是共享的。

这个例子是线程共享变量之后带来的问题

import threading

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

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

def run_thread(n):
    for i in range(100000):
        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)

每次运行输出结果都是不一样的:0,8,--8等等;与预期的balance恒为0不同

原因就是t1和t2线程交替执行,变量共享导致了结果的多变性。

从代码看,t1先于t2执行;从结果看,实际上线程的调度由CPU决定的。

 

解决方法:

解决线程变量共享的关键是,当线程实例创建并调用函数时,给这个函数上锁.t1这个线程使用结束后释放锁,t2再获得锁释放锁.....

import threading

# 假定这是你的银行存款:
balance = 0
lock = threading.Lock()  #创建一个线程锁对象
def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(100000):
        lock.acquire()   #请求锁
        try:
            change_it(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)

线程锁并不作用于函数上,还是线程上,因为函数实际上还是由线程实例执行。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

 

 

不管你的CPU多少核心,Python并不能真正的做到多线程并发。

Python中由于使用了全局解释锁(GIL)的原因,代码并不能同时在多核上并发的运行,也就是说,Python的多线程不能并发,很多人会发现使用多线程来改进自己的Python代码后,程序的运行效率却下降了。这篇文章对Python中的全局解释锁(GIL)进行了介绍。作者认为这是Python中最令人头疼的问题。

这个文章写得很清晰:最让人头疼的Python问题

听说go语言的并发很强大...但是语法太难qwer...

 








以上是关于Python多进程和多线程的主要内容,如果未能解决你的问题,请参考以下文章

python 多进程和多线程配合

Python多线程和多进程谁更快?

Python 多进程和多线程

122 Python程序中的多进程和多线程

多个请求是多线程吗

python多进程和多线程的区别