天天看点

Python 实现多线程和多进程(1) 1 什么是线程和进程2 实现多线程3 小结

    博主最近做一些深度学习模型,需要大批量处理图片数据,这个时候单线程操作,数据预处理很耗时,因此粗略地学习了下多线程和多进程的知识,写点简单的学习小结,章节构建如下:

目录

1 什么是线程和进程

2 实现多线程

2.1 threading 实现线程操作

2.1.1 添加线程

2.1.2 控制线程

2.2 线程锁 lock 的操作

2.3 GIL 锁

3 小结

1 什么是线程和进程

    对于操作系统而言,一个任务就是一个进程(Process),比方打开一个浏览器,打开一个笔记本。

    有些进程内部同时做多件事情,即同时运行多个子任务,我们把这些子任务称为线程(Thread)。

    所以一个进程至少有一个线程。

    单个 CPU 可以执行多进程和多线程,即由操作系统在多个进程或者线程之间快速切换,使得每个进程或者线程短暂的交替运行。真正实现多线程需要多核 CPU 才可能实现。

    当我们要执行多个任务的时候,我们可以采用多进程、多线程、多进程+多线程的模式来实现。

    但是多个任务间可能有某种关联,需要相互通信和协调,比方我要完成任务1和任务2,才能开始做任务3和任务4。

2 实现多线程

    Python 的标准库提供了两个模块:

_thread 

和 

threading

_thread 

是低级模块,

threading 

是高级模块,对 

_thread 

进行了封装。通常,我们只需要使用 

threading 

这个高级模块。

2.1 threading 实现线程操作

2.1.1 添加线程

    导入线程模块:

import threading
           

    获取已激活的线程数:

threading.active_count()
           

    查看现在正在运行的线程:

threading.current_thread()
           

    添加线程:

thread = threading.Thread(target=thread_job,)
           

注意:接收参数 

target 

代表这个线程要完成的任务,需自行定义

    一段完成的小代码:

import threading

def t_job():
    print('current_thread: %s' % threading.current_thread())

def main():
    thread = threading.Thread(target=t_job,)   
    thread.start()  
    
if __name__ == '__main__':
    main()
           
current_thread: <Thread(Thread-8, started 15240)>
           

注意:在 windows 平台下,线程(进程)的操作一定要放在 if __name__ == '__main__': 语句下执行。

2.1.2 控制线程

    线程开始运行:

thread.start()
           

    控制多个线程的执行顺序:

thread.join()
           

    为什么要控制多个线程的执行顺序?

    以下面的例子为例介绍:

    假设有 t1_job 和 t2_job 两个任务,第一个任务执行时间10s,第二个任务执行时间1s。创建以下代码:

import time
import threading

def t1_job():
    print("T1 start\n")
    for i in range(10):
        time.sleep(1)
    print("T1 finish\n")

def t2_job():
    print("t2 start\n")
    for i in range(10):
        time.sleep(0.1)
    print("t2 finish\n")

thread_1 = threading.Thread(target=t1_job, name='t1')
thread_2 = threading.Thread(target=t2_job, name='t2')
thread_1.start() 
thread_2.start() 
print("all jobs finished\n")
           

    我们希望得到的输出是:

T1 start
T2 start
T2 finish
T1 finish
all jobs finished
           

    然而实际的输出是:

T1 start
T2 start
T2 finish
all jobs finished
T1 finish
           

    这种杂乱的执行方式是我们不能忍受的,因此要使用 

join() 

加以控制,推荐将每个线程对应的 join() 依次放在所有 start() 后面。

    可将代码修改如下:

import time
import threading

def t1_job():
    print("T1 start\n")
    for i in range(10):
        time.sleep(1)
    print("T1 finish\n")

def t2_job():
    print("t2 start\n")
    for i in range(10):
        time.sleep(0.1)
    print("t2 finish\n")

thread_1 = threading.Thread(target=t1_job, name='t1')
thread_2 = threading.Thread(target=t2_job, name='t2')
thread_1.join() 
thread_2.join() 
print("all jobs finished\n")
           

2.2 线程锁 lock 的操作

    在多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。举个栗子:

import time, threading

c = 0

def t_job(n):
    global c
    c += n
    c -= n

def t_run(n):
    for i in range(1000):
        t_job(n)

t1 = threading.Thread(target=t_run, args=(5,))
t2 = threading.Thread(target=t_run, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(c)
           

注意:args 后面传到的参数需要加个逗号

    我们定义了一个共享变量 

c

,初始值为

,并且启动两个线程,先存后取,理论上结果应该为

,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,

的结果就不一定是

了。

    因为当 CPU 运行时候:

c += n
           

    相当于:

tmp = c + n
c = tmp
           

    线程交替运行时:

初始值 c = 0

t1: tmp1 = c + 5  # tmp1 = 0 + 5 = 5

t2: tmp2 = c + 8  # tmp2 = 0 + 8 = 8
t2: c = tmp2      # c = 8

t1: c = x1        # c = 5
t1: tmp1 = c - 5  # tmp1 = 5 - 5 = 0
t1: c = tmp1      # c = 0

t2: tmp2 = c - 8  # tmp2 = 0 - 8 = -8
t2: c = tmp2      # c = -8

结果 c = -8
           

     如果我们要确保 

计算正确,就要给 

t_job()

上一把锁,当某个线程开始执行 

t_job()

时,该线程因为获得了锁,因此其他线程不能同时执行

t_job()

,只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。

    创建一个锁:

threading.Lock()
           
c = 0
lock = threading.Lock()

def t_run(n):
    for i in range(10000):
        lock.acquire()
        try:
            t_job(n)
        finally:
            lock.release()
           

    当多个线程同时执行

lock.acquire()

时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

    注意:获得锁的线程用完后一定要释放锁,否则其他等待锁的线程将成为死线程。所以我们用

try...finally

来确保锁一定会被释放。

    锁的应用使得单线程能从头到尾不受干扰进行,但是也阻碍了多线程的进行。包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。另外,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

2.3 GIL 锁

    Python 的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何 Python 线程执行前,必须先获得 GIL 锁,然后,每执行100条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在 Python 中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

3 小结

    多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。Python 解释器由于设计时有 GIL 全局锁,导致了多线程无法利用多核。