引言
多任务可以通过"多进程"完成,也可以通过"多线程"完成。
线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,python 当然也不例外,而且 python 的线程是真正的 Poxis Tread ,是真正的线程,而不是模拟出来的线程。
python 内置的线程模块
python 内置了两个模块
_thread
-
threading
其中_thread
是低级模块
threading
是高级模块,threading
模块对_thread
进行了封装。大多数情况下,使用
threading
这个高级模块。
如何启动线程
启动一个线程就是把一个函数传入,并且创建一个Thread
实例,然后再调用start()
开始执行。
如:
t=threading.Thread(target=loop,name='LoopThread')
t.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))
thime.sleep(1)
print('thread %s ended' %threading.current_thread())
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)
我们来看下实际案例:
我们的案例是开启了一个子线程,让子线程去执行 loop() 函数,当子线程执行完后,打印主线程执行完毕。
任何进行默认都会启动一个线程,我们把该线程成为主线程,主线程又可以启动新的线程;
python 的 threading 模块中有一个current_thread()
函数,他永远返回当前线程的实例,主线程的名字叫MainThread
,子线程名字在创建的时候指定,上面例子中,我们是以LoopThread
的名字来指定的。
名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字,python会自动给线程起名为
Thread-1,Thread-2....
Lock 锁
在「多进程」中,同一个变量各有一份拷贝,不同进程之间是不能很方便的操作另外的进程的数据,所以这个时候,我们基本不需要考虑并发的问题。
而在「多线程」中,所有变量被多个线程共享,任何一个线程都可以访问、操作这个变量
因此,线程之间共享数据最大的危险在于:多个线程同时修改了一个变量,导致内容错乱,程序出现异常。
我们来看一下会出现变量被多个线程修改的案例:
import time,threading
#假设这是你的银行存款
balance=0
def change_balance(n):
#一个没有意义的操作,现存后取,如果对于单个线程来说,没有什么问题
#但是对于多线程,问题就有可能发生
global balance#全局可使用
balance=balance+n
balance=balance-n
def run_thread(n):
for i in range(100000):
change_balance(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('End,balance=(%s)'%balance)
我们看一下运行结果
注意:balance 变量需要声明为共享变量,才能被使用
声明方式:
global balance
为什么会出现错乱的情况呢?
因为高级语言的一条语句在 CPU 中的执行其实会变成多条语句,即使是一个很简单的计算
如上边我们的
balance=balance+n
也是会分两步:
- 计算 balance + n ,存入临时变量中:可以看成 x=balance+n
- 将临时变量的值复制给 balance:可以看成balance=x
而此时 x 是局部变量,两个线程都用于一个 x 变量,让代码正常执行时,两个线程交替执行,那么就有可能出现上方例子的情形。
所以为了避免上方情况出现,我们需要确保在一个线程修改 balance 值的情况下,不能让另一个线程干预。
这个时候就需要用到新的概念「锁」
我们需要给上方的 change_balance 这个函数添加一把锁,这样之后,其中一个线程获得了这个锁,除非这个锁被这个线程释放,否则别的线程都无法干预,只能等待。
由于锁只有一个,所以在同一时刻,无论程序有多少线程,同一时刻最多只有一个线程持有该锁,所以就避免了修改的冲突。
创建一个锁的方法:
lock = threading.lock()
获得锁
lock.acquire()
释放锁
lock.release()
我们把上方的代码稍微修改一下:
balance=0
#创建锁
lock=threading.Lock()
def run_thread(n):
for i in range(100000):
#获取锁
lock.acquire()
try:
change_balance(n)
finally:
#改完了一定要释放锁
lock.release()
这样改完之后,我们再来看一下结果:
可见,我们的执行结果全都是 0 ,这样我们的共享变量就不会因为多线程并发而出问题了。
当多个线程同时调用 lock.acquire() 时,只有一个线程能成功获取到锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整的执行,但是坏处也有很多:
首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式进行,效率大大下降。
其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
知识扩展:GIL锁
通过下面这个代码,可以拿到当前系统的 CPU 的核心数
multiprocessing.cpu_count()
在python中启动与CPU核心数量相同的N个线程,在N个线程上都进行死循环,在4核CPU上可以监控到CPU占有率仅有102%,也就是仅使用了1核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑400%,8核就跑800%,那么为什么Python不行呢?
GIL锁
因为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锁,互不影响。
网友评论