多线程
任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。Python的标准库提供了threading模块。
创建
在python中多线程的实现方法有2种。
- 将要执行的方法作为参数传递给Thread的构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14import threading
def worker(args):
print("开始子进程 {0}".format(args))
print("结束子进程 {0}".format(args))
if __name__ == '__main__':
print("start main")
t1 = threading.Thread(target=worker, args=(1,))
t2 = threading.Thread(target=worker, args=(2,))
t1.start()
t2.start()
print("end main")
输出结果1
2
3
4
5
6start main
开始子进程 1
结束子进程 1
开始子进程 2
结束子进程 2
end main
- 从Thread继承,并重写run()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import threading
import time
class Hello(threading.Thread):
def __init__(self, args):
super(Hello, self).__init__() #注意:要显式的调用父类的初始化函数
self.args = args
def run(self):
print("开始子进程 {0}".format(self.args))
time.sleep(1)
print("结束子进程 {0}".format(self.args))
if __name__ == '__main__':
a = 1
print("start main")
t1 = Hello(1)
t2 = Hello(2)
t1.start()
t2.start()
print("end main")
输出结果1
2
3
4
5
6start main
开始子进程 1
开始子进程 2
end main
结束子进程 1
结束子进程 2
模块函数
- threading.active_count()
返回当前存活的thread对象的数量。返回的数量和enumerate()
列表长度相等。 - threading.current_thread()
返回当前线程对象。 - threading.get_ident()
返回当前线程的thread identifier
。这是个非零的整数。该值无特殊含义。 - threading.enumerate()
以列表形式返回当前存活的所有线程对象。列表中包括守护线程和由current_thread()
创建的虚拟线程对象,不包括已结束和还没开始的线程。 - threading.main_thread()
返回主线程对象。正常情况下,主线程由python解释器启动。对象方法
- start()
启动线程,调用run方法。同一线程对象多次调用将会出现RuntimeError
错误。 - run()
启动线程 - join(timeout=None)
等待线程对象结束。 - is_alive()
线程是否活着对象属性
- name
线程的名称,无特殊意义。 - daemon
布尔值,表示该线程是否为守护线程。设置该值必须在调用run()方法前,否则会报RuntimeError
错误。假如主线程不是守护线程,该主线程创建的所有子线程该值为False。线程锁
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
这种情况下通过线程锁保证修改不会发生冲突。
锁的创建通过threading.Lock()
进行,操作类似于多进程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import threading
import time
def worker(name, lock):
with lock:
print("start {0}".format(name))
time.sleep(5)
print("end {0}".format(name))
if __name__ == "__main__":
lock = threading.Lock()
t1 = threading.Thread(target=worker, args=("worker1", lock))
t2 = threading.Thread(target=worker, args=("worker2", lock))
t1.start()
t2.start()
输出结果1
2
3
4start worker1
end worker1
start worker2
end worker2
当然,获取锁也可以通过lock.acquire()
完成,释放锁通过lock.release()
完成。1
2
3
4
5
6
7
8def worker(name, lock):
lock.acquire()
try:
print("start {0}".format(name))
time.sleep(5)
print("end {0}".format(name))
finally:
lock.release()
- 锁的好处
确保了某段关键代码只能由一个线程从头到尾完整地执行。 - 坏处
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。线程共享内存
多线程和多进程不同之处在于多线程本身就是可以和父进程共享内存的,这也是为什么其中一个线程挂掉以后,为什么其他线程也会死掉的道理。1
2
3
4
5
6
7
8
9
10
11
12
13
14import threading
l = list()
l += range(1, 4)
def worker():
l.append("GOD")
if __name__ == "__main__":
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start()
t2.start()
print(l)
输出结果1
[1, 2, 3, 'GOD', 'GOD']
线程池
在使用多线程处理任务时也不是线程越多越好,由于在切换线程的时候,需要切换上下文环境,依然会造成cpu的大量开销。为解决这个问题,线程池的概念被提出来了。预先创建好一个较为优化的数量的线程,让过来的任务立刻能够使用,就形成了线程池。
此处介绍threapool
是一个第三方模块,需要使用pip install threadpool
安装。1
2
3
4
5
6
7
8
9
10
11
12
13
14import threadpool
def hello(m, n, o):
print("m = {0} n={1} o={2}".format(m, n, o))
if __name__ == '__main__':
lst_vars_1 = ['1', '2', '3']
lst_vars_2 = ['4', '5', '6']
func_var = [(lst_vars_1, None), (lst_vars_2, None)]
pool = threadpool.ThreadPool(2)
requests = threadpool.makeRequests(hello, func_var)
[pool.putRequest(req) for req in requests]
pool.wait()
输出结果1
2m = 1 n=2 o=3
m = 4 n=5 o=6
- Executor
在python3.3之后,并行任务可以使用Executor Objects
,更多内容点击concurrent.futures — Launching parallel tasks查看官方介绍。 - 关于python中多核CPU的说明
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
更多关于threading
的内容可以点击threading — Thread-based parallelism查看官方介绍。