Python中的多线程

即在一个进程中启动多个线程执行任务。一般来说使用多线程可以达到并行的目的,但由于Python中使用了全局解释锁GIL的概念,导致Python中的多线程并不是并行执行,而是“交替执行
所以Python中的多线程适合IO密集型任务,而不适合计算密集型任务。

Python提供两组多线程接口,一是thread模块_thread,提供低等级接口。二是threading模块,提供更容易使用的基于对象的接口,可以继承Thread对象来实现线程,此外其还提供了其它线程相关的对象,例如Timer,Lock等。

重写run方法

import threading
import time

class MyThread(threading.Thread):
    def run(self):
        thread_id = threading.get_ident()
        current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print("线程ID:{},当前时间:{}".format(thread_id, current_time))

if __name__ == "__main__":
    for i in range(5):
        t = MyThread()
        t.start()

多线程实例

import time
import random
from threading import Thread

def down_load(fine_name):
    print(f'开始下载{fine_name}...')
    start_time = time.time()
    second = random.randint(5,10)
    time.sleep(second)
	end_time = time.time()
    Total_time = end_time - start_time
    print(f'下载完成,耗时为:{Total_time:.2f}/s')


def main():
    zong_start_time = time.time()
    a = 'python 从入门到实践'
    b = 'JAVA从入门到实践'
    t1 = Thread(target=down_load,args=(a,))        #线程1
    t1.start()
    t2 = Thread(target=down_load,args=(b,))       #线程2
    t2.start()
    t1.join()
    t2.join()	
    zong_end_time = time.time()
	print(f'总计耗时{zong_end_time - zong_start_time:.2f}/s')


if __name__ == '__main__':
    main()

线程池 concurrent.futures

import time
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED


def work():
    print('work')
    time.sleep(1)


task = ThreadPoolExecutor(5)
t = []
for n in range(10):
    t.append(task.submit(work))

wait(t, return_when=ALL_COMPLETED)  # 等待所有子线程执行完成后,继续执行主线程
# wait(t, return_when=FIRST_COMPLETED)  # 第一个任务完成后执行主线程
print(1111)

获取返回值

同步

# coding: utf-8
import threading
import time
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, as_completed, FIRST_COMPLETED


def work():
    print('work')
    time.sleep(1)


task = ThreadPoolExecutor(5)
for n in range(10):
    result = task.submit(work)
    print(result.result())   # 这种方法和同步没啥区别,

异步

# coding: utf-8
import threading
import time
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED, as_completed, FIRST_COMPLETED


def work():
    print('work')
    time.sleep(1)


task = ThreadPoolExecutor(5)
t = []
for n in range(10):
    t.append(task.submit(work))

for i in as_completed(t):
    r = i.result()
    print(r)
print(1111)

Python中的多进程

由于Python中GIL的原因,对于计算密集型任务,Python下比较好的并行方式是使用多进程,这样可以非常有效的使用CPU资源。当然同一时间执行的进程数量取决你电脑的CPU核心数。
Python中的进程模块为mutliprocess模块,提供了很多容易使用的基于对象的接口。另外它提供了封装好的管道和队列,可以方便的在进程间传递消息。Python还提供了进程池Pool对象,可以方便的管理和控制线程。

"""
创建和使用线程

进程:操作系统分配内存的基本单元,每个进程的内存空间是独立的,进程之间不共享内存
线程:操作系统分配CPU的基本单元,如果程序使用了多线程会比单线程占用更多的CPU资源
一个程序通常是一个或多个进程,一个进程可以包含一个或多个线程

可以通过Windows的任务管理器或macOS系统的活动监视器来查看进程

图形图像处理、音视频压缩解码、科学计算 --->计算密集型任务 ---> 多进程
联网获取数据、文件读写 ---> I/O密集型任务 ---> 多线程 / 异步编程
Python ---> CPython ---> GIL(全局解释器锁)---> 无法发挥多核多CPU优势
"""
import random
import time
from multiprocessing import Process

def upload(filename):
    print(f'开始上传{filename}')
    time.sleep(random.randint(3, 8))
    print(f'{filename}上传完成')

def download(filename):
    print(f'开始下载{filename}')
    time.sleep(random.randint(5, 10))
    print(f'{filename}下载完成')

def main():
    start = time.time()
    t1 = Process(target=download, args=('Python从入门到精通', ))            #进程1
    t1.start()
    t2 = Process(target=download, args=('深入浅出MySQL', ))                 #进程2
    t2.start()
    t3 = Process(target=upload, args=('Python网络爬虫开发', ))				#进程3
    t3.start()
    t1.join()
    t2.join()
    t3.join()
    end = time.time()
    print(f'总共耗时: {end - start:.3f}秒')


if __name__ == '__main__':
    main()

重写run

import os
import time
from multiprocessing import Process

class MyProcess(Process):
    def run(self):
        process_id = os.getpid()
        current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        print("进程ID:{},当前时间:{}".format(process_id, current_time))

if __name__ == "__main__":
    processes = []
    for i in range(5):
        p = MyProcess()
        processes.append(p)
        p.start()
    for p in processes:
        p.join()

进程池

from multiprocessing import Pool
import os,time,random
 
def worker(msg):
    t_start = time.time()
    print("%s开始执行,进程号为%d"%(msg,os.getpid()))
    #random.random()随机生成0~1之间的浮点数
    time.sleep(random.random()*2) 
    t_stop = time.time()
    print(msg,"执行完毕,耗时%0.2f"%(t_stop-t_start))
 
po=Pool(3) #定义一个进程池,最大进程数3
for i in range(0,10):
    #Pool.apply_async(要调用的目标,(传递给目标的参数元祖,))
    #每次循环将会用空闲出来的子进程去调用目标
    po.apply_async(worker,(i,))
 
print("----start----")
po.close() #关闭进程池,关闭后po不再接收新的请求
po.join() #等待po中所有子进程执行完成,必须放在close语句之后
print("-----end-----")

multiprocessing.Pool常用函数解析:

  • apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
  • apply_async(func[, args[, kwds]]) :使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
  • close():关闭Pool,使其不再接受新的任务;
  • terminate():不管任务是否完成,立即终止;
  • join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;

案例
如果频繁去创建一个进程,然后销毁它,会导致性能下降。对于这种情况推荐的做法是事先创建一个进程池,在有任务达到时从进程池中取出一个进程来执行相关任务,在任务完成后便归还回去。这样做可以复用部分已有进程资源,达到提升效率的作用。

在 mulitprocessing 模块中有一个 Pool 类可以帮助我们完成该任务。下面是一个使用进程池的例子。

import multiprocessing             # 引入multiprocessing模块
import time                        # 引入time模块
def child_process_entry():         # 定义进程入口函数
    print(u"子进程在运行")
    time.sleep(10)
    print(u"子进程结束")
pool_obj = multiprocessing.Pool(processes = 5)  # 建立5个元素池子
for i in range(10):                             # 添加10个进程
    pool_obj.apply_async(child_process_entry)
    # pool_obj.apply(child_process_entry, args=(i, ))  # 同步
pool_obj.close()                                # 停止添加进程了
pool_obj.join()                                 # 等待进程都结束

进程通信

在一些复杂的应用场景,则希望进程之间有更多的信息交换。下面介绍 multiprocessing 模块提供的进程之间通信的方式,包括管道、队列和锁。

  • 管道

管道有两头,一般一头给进程 A,一头给进程 B。如果进程 A 对管道进行写入操作,那么进程B就可以通过读操作看到写入的数据。而且管道是双向的,可以进程 A 写进程 B 读,也可以进程 B 写进程 A 读。

import multiprocessing                        # 引入multiprocessing模块
def process_A(pipe):                          # 进程A的入口函数
    print(u"进程A发送数据hello到B")            # 发送数据hello到进程B
    pipe.send('hello')
    print(u"进程A等待进程B的输入")
    data = pipe.recv()                        # 结束进程B的数据
    print(u'进程A收到B的数据%s' % data)
    print(u'进程A结束')
def process_B(pipe):                          # 进程B的入口函数
    print(u'进程B等待进程A的数据')
    data = pipe.recv()                        # 接收数据
    print(u'进程B收到B的数据%s' % data)
    print(u'进程B发送hi到进程A')
    pipe.send('hi')                           # 发送数据
    print(u'进程B结束')
pipe = multiprocessing.Pipe()                 # 建立管道
        # 创建进程A
p1 = multiprocessing.Process(target=process_A, args=(pipe[0],))
        # 创建进程B
p2 = multiprocessing.Process(target=process_B, args=(pipe[1],))
p1.start()                                    # 启动进程
p2.start()
p1.join()                                     # 等待进程结束
p2.join()

案例2

import multiprocessing
import random
import time, os


def proc_send(pipe, urls):
    for url in urls:
        print("Process (%s) send: %s" % (os.getpid(), url))
        pipe.send(url)
        time.sleep(random.random())


def proc_recv(pipe):
    while True:
        print("Process (%s) rev: %s" % (os.getpid(), pipe.recv()))
        time.sleep(random.random())


if __name__ == '__main__':
    pipe = multiprocessing.Pipe()
    p1 = multiprocessing.Process(target=proc_send, args=(pipe[0], ['url_' + str(i) for i in range(10)]))
    p2 = multiprocessing.Process(target=proc_recv, args=(pipe[1],))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
  • 队列
    对于队列有两个操作,一个是写入数据,一个是读出数据。最简单的使用方式是让一个进程往队列中写入数据,让另外一个进程从队列中读出数据。
import multiprocessing
  import time
  def process_read(queue):                      # 读队列子进程
      print(u"queue读出进程开始运行")
      data = queue.get()
      while(data != "quit"):                    # 一直读,直到读出了quit
          print("queue读出进程读出数据%s" % data)
          data = queue.get()
      print(u'queue读出进程退出')
  def process_write(queue):                     # 写队列子进程
      print(u"queue写入进程开始运行")
      data = ['good', 'morning', 'everyone']    # 写入的数据
      for w in data:
          print("queue写入进程写入数据%s" % w)
          data = queue.put(w)
          time.sleep(1)
      data = queue.put('quit')                  # 发送quit,让读子进程退出
      print(u'queue写入进程退出')
  queue_obj = multiprocessing.Queue(3)
  # 创建两个子进程,一个读队列,一个写队列
  p1   = multiprocessing.Process(target=process_read, args=(queue_obj,))
  # Pass the other end of the pipe to process 2
  p2   = multiprocessing.Process(target=process_write, args=(queue_obj,))
  p1.start()                                    # 启动进程
  p2.start()     
  p1.join()                                     # 等待子进程退出
  p2.join()
  queue_obj.close()                             # 销毁队列

Lock锁

该对象主要提供了两个接口函数,一个是得到锁 acquire(),另外一个是释放锁 release()。下面的例子演示了 3 个进程,它们都往同一个文件写入数据。由于其每轮操作需要执行 3 次数据写入,而且要求在这 3 次操作之间其他的进程不能往文件写入数据。

import multiprocessing
import time
def process_entry (lock, fd, id):               # 进程入口函数
    for x in range(30):                                 # 30轮输出,每轮进行3次写操作
        lock.acquire()                                  # 得到锁
        line = "%d: line 1, round: %d\n" % (id, x)
        time.sleep(0.1)
        fd.write(line)                                  # 第1次写
        fd.flush()
        line = "%d: line 2, round: %d\n" % (id, x)
        fd.write(line)                                  # 第2次写
        fd.flush()
        time.sleep(0.1)
        line = "%d: line 3, round: %d\n" % (id, x)
        fd.write(line)                                          # 第3次写
        fd.flush()     
        lock.release()                                          # 释放锁
        time.sleep(0.1)
if __name__ == "__main__":
    file_name = "shared_input2.txt"
    fd = open(file_name, "a+")
    lock = multiprocessing.Lock()                       # 创建锁
    p1 = multiprocessing.Process(target=process_entry, args=(lock, fd, 1))
    p2 = multiprocessing.Process(target=process_entry, args=(lock, fd, 2))
    p3 = multiprocessing.Process(target=process_entry, args=(lock, fd, 3))
    p1.start()                                                          # 启动进程
    p2.start()
    p3.start()
    p1.join()                                                           # 等待子进程结束
    p2.join()
    p3.join()
    fd.close()                                                          # 关闭文件
    print(u"开始检查结果")                                    # 开始检查结果
    fd2 = open(file_name, "r")
    lines = fd2.readlines()
    fd2.close()
    line_num = len(lines)                                       # 显示行数
    print(u"总的行数: %d" % line_num)
            # 要求每连续的3行是同一个进程打印出来的
    for x in range(int(line_num/3)):
                # 第1行和第2行不是同一个进程打印出来的
        if lines[x*3][:2] != lines[x*3+1][:2]:
            print("line %d: Error" % x*3+1)     # 发现错误,退出
            sys.exit(1)
                # 第1行和第3行不是同一个进程打印出来的
        if lines[x*3][:2] != lines[x*3+2][:2]:
            print("line %d: Error" % x*3+2)     # 发现错误,退出
            sys.exit(1)
    print(u"成功通过检查")

也可以使用 with lock 语句,这样就不用再显式调用 acquire() 和 release() 了,只需将 acquire() 和 release() 之间的代码放到 with lock 块中即可。