Contents
- 1 进程与线程
- 1.1 多进程与多线程
- 1.2 为何需要多线程(多进程)
- 2 多进程
- 2.1 pool 创建大量子进程
- 2.2 子进程
- 2.3 进程间通信
- 3 多线程
- 3.1 Lock
- 4 多进程 vs 多线程
- 5 全局锁问题
进程与线程
进程和线程是操作系统层面的概念,本质上就是两个操作系统内核对象:即操作系统定义的两个数据结构,操作系统通过这两个数据结构,来管理程序的运行。 (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……
另一种多线程示例代码如下:
代码语言:javascript复制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 编程(不适合初学者)的可以学习下这本书。网上链接在这里。