day9-01 线程

多任务能够由多进程完成,也能够由一个进程内的多线程完成。python

咱们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。编程

因为线程是操做系统直接支持的执行单元,所以,高级语言一般都内置多线程的支持,Python也不例外,而且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。多线程

Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数状况下,咱们只须要使用threading这个高级模块。并发

启动一个线程就是把一个函数传入并建立Thread实例,而后调用start()开始执行:ide

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(1)
    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 
>>> 1thread LoopThread 
>>> 2thread LoopThread 
>>> 3thread LoopThread 
>>> 4thread LoopThread 
>>> 5thread LoopThread ended.
thread MainThread ended.

因为任何进程默认就会启动一个线程,咱们把该线程称为主线程,主线程又能够启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在建立时指定,咱们用LoopThread命名子线程。名字仅仅在打印时用来显示,彻底没有其余意义,若是不起名字Python就自动给线程命名为Thread-1Thread-2……oop

Lock

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

来看看多个线程同时操做一个变量怎么把内容给改乱了:操作系统

import time, threading
# 假定这是你的银行存款:
balance = 0def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - ndef run_thread(n):
    for i in range(100000):
        change_it(n)

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

咱们定义了一个共享变量balance,初始值为0,而且启动两个线程,先存后取,理论上结果应该为0,可是,因为线程的调度是由操做系统决定的,当t一、t2交替执行时,只要循环次数足够多,balance的结果就不必定是0了。线程

缘由是由于高级语言的一条语句在CPU执行时是若干条语句,即便一个简单的计算:

balance = balance + n

也分两步:

  1. 计算balance + n,存入临时变量中;

  2. 将临时变量的值赋给balance

也就是能够当作:

x = balance + n
balance = x

因为x是局部变量,两个线程各自都有本身的x,当代码正常执行时:

初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0

结果 balance = 0

可是t1和t2是交替运行的,若是操做系统如下面的顺序执行t一、t2:

初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5
t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8
t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0
t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2   # balance = -8
结果 balance = -8

究其缘由,是由于修改balance须要多条语句,而执行这几条语句时,线程可能中断,从而致使多个线程把同一个对象的内容改乱了。

两个线程同时一存一取,就可能致使余额不对,你确定不但愿你的银行存款莫名其妙地变成了负数,因此,咱们必须确保一个线程在修改balance的时候,别的线程必定不能改。

若是咱们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,咱们说,该线程由于得到了锁,所以其余线程不能同时执行change_it(),只能等待,直到锁被释放后,得到该锁之后才能改。因为锁只有一个,不管多少线程,同一时刻最多只有一个线程持有该锁,因此,不会形成修改的冲突。建立一个锁就是经过threading.Lock()来实现:

balance = 0
lock = threading.Lock()
def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了必定要释放锁:
            lock.release()

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,而后继续执行代码,其余线程就继续等待直到得到锁为止。

得到锁的线程用完后必定要释放锁,不然那些苦苦等待锁的线程将永远等待下去,成为死线程。因此咱们用try...finally来确保锁必定会被释放。

锁的好处就是确保了某段关键代码只能由一个线程从头至尾完整地执行,坏处固然也不少,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地降低了。其次,因为能够存在多个锁,不一样的线程持有不一样的锁,并试图获取对方持有的锁时,可能会形成死锁,致使多个线程所有挂起,既不能执行,也没法结束,只能靠操做系统强制终止。

多核CPU

若是你不幸拥有一个多核CPU,你确定在想,多核应该能够同时执行多个线程。

若是写一个死循环的话,会出现什么状况呢?

打开Mac OS X的Activity Monitor,或者Windows的Task Manager,均可以监控某个进程的CPU使用率。

咱们能够监控到一个死循环线程会100%占用一个CPU。

若是有两个死循环线程,在多核CPU中,能够监控到会占用200%的CPU,也就是占用两个CPU核心。

要想把N核CPU的核心所有跑满,就必须启动N个死循环线程。

试试用Python写个死循环:

import threading, multiprocessing
def loop():
    x = 0
    while True:
        x = x ^ 1for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

启动与CPU核心数量相同的N个线程,在4核CPU上能够监控到CPU占用率仅有102%,也就是仅使用了一核。

可是用C、C++或Java来改写相同的死循环,直接能够把所有核心跑满,4核就跑到400%,8核就跑到800%,为何Python不行呢?

由于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锁,互不影响。

小结

多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要当心死锁的发生。

Python解释器因为设计时有GIL全局锁,致使了多线程没法利用多核。多线程的并发在Python中就是一个美丽的梦

相关文章
相关标签/搜索