真锋
永远保持一颗学习和专注的心
嵌入式视觉笔记

Python3 多进程与多线程

进程与线程

进程和线程是操作系统层面的概念,本质上就是两个操作系统内核对象:即操作系统定义的两个数据结构,操作系统通过这两个数据结构,来管理程序的运行。
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

多进程与多线程

从概念上讲,对于操作系统来说,一个任务就是一个进程(Process),而进程内的”子任务”称为线程(Thread),一个进程至少有一个线程。具有多核cpu的电脑,可以真正实现物理上的多进程。 多任务的实现有3种方式:

  • 多进程模式;
  • 多线程模式;
  • 多进程+多线程模式。

多进程与多线程的程序涉及到同步、数据共享的问题,所以程序编写更复杂些。

为何需要多线程(多进程)

多线程(多进程)能让我们实现并发编程,并发技术,就是可以让我们在同一时间同时执行多条任务的技术。

多进程

Python3 实现多进程(multiprocessing),对于 linux 系统可以直接使用 fork() 调用,windows 系统可以使用内置 multiprocessing 模块。创建进程的代码一定要放在 if __name__ == '__main__' 里面。multiprocessing 模块简单例子如下:

from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
    print('RUn child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
    print('Paraent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test', ))
    print('Child process will start')
    p.start()
    p.join()
    print('Child process end.')

Paraent process 9400.

Child process will start

RUn child process test (9451)…

Child process end.

os.getpid()函数返回当前进程pid.

pool 创建大量子进程

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 9400.
Run task 0 (10364)…
Run task 2 (10366)…
Run task 1 (10365)…
Run task 3 (10367)…
Waiting for all subprocesses done…
Task 3 runs 0.73 seconds.
Run task 4 (10367)…
Task 1 runs 1.16 seconds.
Task 2 runs 1.33 seconds.
Task 4 runs 0.59 seconds.
Task 0 runs 2.60 seconds.
All subprocesses done.

对 pool 对象调用 join() 方法会等待所有子进程执行完毕,然后执行后续语句,调用 join() 方法之前必须调用 close(),调用 close() 之后就不能继续添加新的 Process 了。

子进程

进程间通信

Python 的 multiprocessing 模块包装了底层机制,提供了 Queue、Pipes 等多种方式来交换数据。

多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。启动一个线程就是把一个函数传入并创建 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))
          time.sleep(2)
    print('thread %s ended.' % threading.current_thread().name)
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)

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()函数,它永远返回当前线程的实例。主线程实例的名字叫 MainThread,子线程的名字在创建时指定,我们用 LoopThread 命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字 Python 就自动给线程命名为 Thread-1, Thread-2……

另一种多线程示例代码如下:

import threading
def doubler(number):
    """
    可以被线程使用的一个函数
    """
    print(threading.currentThread().getName() + '\n')
    print(number * 2)
    # print()
if __name__ == '__main__':
    for i in range(5):
        my_thread = threading.Thread(target=doubler, args=(i,))
        my_thread.start()

代码运行后,输出如下

Thread-1
0
Thread-2
2
Thread-3
4
Thread-4
6
Thread-5
8

Lock

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

创建线程锁通过 threading.Lock() 来实现,获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用 try...finally 机制来确保锁一定会被释放。

多进程 vs 多线程

Python 中的多线程最好用于处理有关 I/O 的操作,如从网上下载资源或者从本地读取文件或者目录。如果你要做的是 CPU 密集型操作,那么你需要使用 Python 的 multiprocessing 多进程模块。这样做的原因是,Python 有一个全局解释器锁 (GIL),使得所有子线程都必须运行在同一个主线程中, GIL 导致了 导致 Python 中的多线程并不是并行执行,而是“交替执行” 。正因为如此,当你通过多线程来处理多个 CPU 密集型任务时,你会发现它实际上运行的更慢。因此,我们将重点放在那些多线程最擅长的领域:I/O 操作

而所谓 Python 多线程适合 I/O 密集型任务,指的是,当一个线程处于 IO 阻塞状态时会释放 GIL 锁,那么这个时候其他线程就可以获得锁然后进行发送数据,当这个线程发送完处于 IO 阻塞后,又可以被第三个线程拿到 GIL 锁进行 IO 发送数据的操作,所以一个时间片内会出现一个线程在发送数据,另个线程在传输数据,这样就减少了 IO 传输时间

全局锁问题

Python 的全局锁问题 (GIL),我在网上看过很多文章,但是大多数泛泛而谈,没有经过认真思考和实践总结而写的片面性文章,直到 2020.11.16 日这天阅读《Python + Cookbook》第三版这本书籍,才开始慢慢理解 GIL 和多线程的关系。

值得一提的是,这本书翻译的质量确实不好,但是类似的书籍我目前也没有找到,想要深入学习 Python 编程(不适合初学者)的可以学习下这本书。网上链接在这里

赞赏

发表评论

textsms
account_circle
email

嵌入式视觉笔记

Python3 多进程与多线程
进程与线程 进程和线程是操作系统层面的概念,本质上就是两个操作系统内核对象:即操作系统定义的两个数据结构,操作系统通过这两个数据结构,来管理程序的运行。(1)以多进程形式,…
扫描二维码继续阅读
2019-06-09