Python 的多线程编程:基础

说说 Python 的多线程

为什么需要线程

Python 是提供多线程支持的,但是使用多线程的场景在哪里?比如说你的程序中有一项任务需要在执行时不影响当前代码的执行,此时多线程就能用得上,也就是借助线程你可以同时并发的执行多个任务,这就是线程的在编程中提供的最重要的功能:并发

执行一个简单的线程

Python 2 中对线程的支持分别来自 threadthreading 模块,由于 thread 在 Python 3 中已经重命名了,所以这里我们始终使用 threading:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading
import time


def myfunc(name, sleep):
while 1:
print(name)
time.sleep(sleep)


if __name__ == '__main__':
threading.Thread(target=myfunc, args=("Thread No 2", 2)).start()
while 1:
pass

在上面的代码中,我们使用 Thread object 开启了一个新的线程并执行,执行代码之后可以看到 “Thread No 2” 每隔 2 秒显示一次,开启线程之后的 while 1 是必须的,因为主线程必须一直存在才能让子线程得以执行。

多线程共享资源保护

多线程执行环境下我们常常需要对共享的资源进行保护以防止多个线程操作同一内存区域而导致的数据错误,因为线程之间是共享内存的,一般通过 lock 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import threading
import time

value = 1

def myfunc(name, sleep):
while 1:
# entering critical section
global value
lock.acquire()
print (name, " Now Sleeping after Lock acquired for ", sleep)
time.sleep(sleep)
value += 1
print(name, "Now releasing lock and then sleeping again")
lock.release()
# exiting critical section
time.sleep(sleep) # yield current thread cpu


if __name__ == '__main__':
lock = threading.Lock()
# why run can't start thread many
# threading.Thread(target=myfunc, args=("Thread No 2", 2)).run()
threading.Thread(target=myfunc, args=("Thread No 2", 2)).start()
threading.Thread(target=myfunc, args=("Thread No 3", 2)).start()
threading.Thread(target=myfunc, args=("Thread No 4", 2)).start()
while 1:
pass

Python 的 lock 通过 threading 模块的 Lock 来实现,对需要共同操作的共享内存区域进行加锁保护,除了 Lock,threading 模块还提供了 Queue、Event、Condition 等机制来保证多线程条件下的同步问题,我会在后续的文章中进行详细阐述。

GIL

Python 解释器不是线程安全的,而 Python 线程没有优先级,没有线程组,线程不能被停止、挂起、恢复、中断,也就是 Python 提供的线程非常基础简单。实际上每次只有一个线程在执行,这是由于 GIL 的存在导致的,为了支持多线程编程,当前执行线程必须获得 global lock 用来保证共享 Python object 的数据安全,如果没有这个锁,两个线程同时增加一个对象的引用计数时,最终这个计数器可能只加了一次,因而只有获得了 GIL 的线程才能执行对 Python Object 进行操作或者调用 Python C API 函数。

为了支持多线程的程序,Python 解释器必须定期释放和重新获取 GIL 锁,默认是每执行 10 bytecode 的指令之后,可以使用 sys.setcheckinterval 来改变这一行为。GIL 锁也可以在程序遇到 IO 阻塞时释放,比如读取和写入文件,这样其他线程在当前线程等待 IO 完成时执行,一般来说有以下几种情形能够释放 GIL 锁:

  • C 扩展
  • 阻塞 IO

这也是为什么使用 C 来完成 Python 程序更高效的原因所在。

方法 join 的作用

上面的例子中我们在主线程中使用了一个 while 1:pass 语句来保证多线程可以顺利执行,除此之外线程本身提供了 join 方法来告诉主线程等待子线程执行结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import threading
import time

value = 1


def myfunc(name, sleep):
while 1:
# entering critical section
global value
if value > 6:
raise Exception("Stop ", name)
lock.acquire()
print (name, " Now Sleeping after Lock acquired for ", sleep)
time.sleep(sleep)
value += 1
print(name, "Now releasing lock and then sleeping again")
lock.release() # release must be called after acquire lock
# exiting critical section
time.sleep(sleep) # yield current thread cpu


if __name__ == '__main__':
lock = threading.Lock()
t1 = threading.Thread(target=myfunc, args=("Thread No 2", 2))
t2 = threading.Thread(target=myfunc, args=("Thread No 3", 2))
t3 = threading.Thread(target=myfunc, args=("Thread No 4", 2))
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print("main Thread echo done!")

join 的作用和 while 1 类似,都是告知调用线程要等待直到线程执行结束或者异常退出,join 还可以接受参数,表示等待多久之后。

多线程执行你需要了解和知道的

1.处理器并不保证在 start 开始之后立即运行 run
2.无法保证线程运行的顺序
3.对于任意线程来说,它保证 run 中的语句是顺序执行的
4.等待 io 阻塞时可以让出 CPU 以让其它线程得到执行

三月沙 wechat
扫描关注 wecatch 的公众号