参考文档:
金角大王博客: http://www.cnblogs.com/alex3714/articles/5230609.html
银角大王博客: http://www.cnblogs.com/wupeiqi/articles/5040827.html
线程是操作系统能够进行运算调度的最小单位. 它被包含在进程之中, 是进程中的实际运作单位. 一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发多个线程, 每条线程并行执行不同的任务
Threading 用于提供线程相关的操作, 线程是应用程序中工作的最小单元.
更多方法:
start 线程准备就绪, 等待 CPU 调度
setName 为线程设置名称
getName 获取线程名称
setDaemon 设置为后台线程或前台线程(默认)
如果是后台线程, 主线程执行过程中, 后台线程也在进行, 主线程执行完毕后, 后台线程不论成功与否, 均停止
如果是前台线程, 主线程执行过程中, 前台线程也在进行, 主线程执行完毕后, 等待前台线程也执行完成后, 程序停
join 逐个执行每个线程, 执行完毕后继续往下执行, 该方法使得多线程变得无意义
run
线程被 cpu 调度后自动执行线程对象的 run 方法
一, 线程有 2 种调用方式:
1, 直接调用
2, 继承式调用
具体实例如下:
直接调用
- import threading,time
- import os,sys
- def sayhi(num):
- print("running on number:%s" % num)
- time.sleep(3)
- if __name__ == '__main__':
- #t1 = threading.Thread(target=sayhi,name='abcd',args=(1,),daemon=True)
- #t2 = threading.Thread(target=sayhi,name='1234',args=(2,),daemon=True)
- ''' t1 = threading.Thread(target=sayhi,name='abcd',args=(1,))
- t2 = threading.Thread(target=sayhi,name='1234',args=(2,))
- t1.start()
- t2.start()
- print(t1.getName())
- print(t2.getName())
直接调用
继承式调用
- import threading,time
- import os,sys
- class MyThread(threading.Thread):
- def __init__(self,num):
- threading.Thread.__init__(self)
- self.num = num
- def run(self):
- print("running on number:%s" % self.num)
- time.sleep(3)
- if __name__ == '__main__':
- t1 = MyThread(1)
- t2 = MyThread(2)
- t1.start()
- t2.start()
- print(t1.getName())
- print(t2.getName())
继承式调用
二, 线程之 join 和 setDaemon
- import threading,time
- def run(n):
- print('[%s]------running-----\n' % n)
- time.sleep(5)
- print('----done----')
- def main():
- for i in range(5):
- t = threading.Thread(target=run,args=(i,))
- t.start()
- #t.join() #阻塞变串行
- #t.join(2) #设置等待超时时间, 和 time.sleep 等待时间, 谁先到谁继续
- #t.join(timeout=9) #超时时间大于 sleep 时间, 没有啥意义
- print('start threading %s' % i,t.getName())
- #main()
- m = threading.Thread(target=main,args=())
- m.setDaemon(True) #将 main 线程设置为 Daemon 线程, 它做为程序主线程的守护线程, 当主线程退出时, m 线程也会退出, 由 m 启动的其它子线程会同时退出, 不管是否执行完任务
- m.start()
- m.join() #变为不阻塞, 但 run 中 sleep 后的输出不会执行
- print("----------main thread done------------")
三, 线程之锁:
Python GIL(Global Interpreter Lock)
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
上面的核心意思就是, 无论你启多少个线程, 你有多少个 cpu, Python 在执行的时候会淡定的在同一时刻只允许一个线程运行, 擦..., 那这还叫什么多线程呀? 莫如此早的下结结论, 听我现场讲.
首先需要明确的一点是 GIL 并不是 Python 的特性, 它是在实现 Python 解析器 (CPython) 时所引入的一个概念. 就好比 C++ 是一套语言 (语法) 标准, 但是可以用不同的编译器来编译成可执行代码. 有名的编译器例如 GCC,INTEL C++,Visual C++ 等. Python 也一样, 同样一段代码可以通过 CPython,PyPy,Psyco 等不同的 Python 执行环境来执行. 像其中的 JPython 就没有 GIL. 然而因为 CPython 是大部分环境下默认的 Python 执行环境. 所以在很多人的概念里 CPython 就是 Python, 也就想当然的把 GIL 归结为 Python 语言的缺陷. 所以这里要先明确一点:
GIL
并不是
Python 的特性, Python 完全可以不依赖于 GIL
这篇文章透彻的剖析了 GIL 对 python 多线程的影响, 强烈推荐看一下: http://www.dabeaz.com/python/UnderstandingGIL.pdf
线程锁(互斥锁 Mutex)
由于线程之间是进行随机调度, 并且每个线程可能只执行 n 条执行之后, 当多个线程同时修改同一条数据时可能会出现脏数据, 所以, 出现了线程锁 - 同一时刻允许一个线程执行操作.
一个进程下可以启动多个线程, 多个线程共享父进程的内存空间, 也就意味着每个线程可以访问同一份数据, 此时, 如果 2 个线程同时要修改同一份数据, 会出现什么状况?
未加锁状态
- import time
- import threading
- def addNum():
- global num #在每个线程中都获取这个全局变量
- print('--get num:',num )
- time.sleep(1)
- num -=1 #对此公共变量进行 - 1 操作
- num = 100 #设定一个共享变量
- thread_list = []
- for i in range(100):
- t = threading.Thread(target=addNum)
- t.start()
- thread_list.append(t)
- for t in thread_list: #等待所有线程执行完毕
- t.join()
- print('final num:', num )
未加锁
正常来讲, 这个 num 结果应该是 0, 但在 python 2.7 上多运行几次, 会发现, 最后打印出来的 num 结果不总是 0, 为什么每次运行的结果不一样呢? 哈, 很简单, 假设你有 A,B 两个线程, 此时都 要对 num 进行减 1 操作, 由于 2 个线程是并发同时运行的, 所以 2 个线程很有可能同时拿走了 num=100 这个初始变量交给 cpu 去运算, 当 A 线程去处完的结果是 99, 但此时 B 线程运算完的结果也是 99, 两个线程同时 CPU 运算的结果再赋值给 num 变量后, 结果就都是 99. 那怎么办呢? 很简单, 每个线程在要修改公共数据时, 为了避免自己在还没改完的时候别人也来修改此数据, 可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据.
* 注: 不要在 3.x 上运行, 不知为什么, 3.x 上的结果总是正确的, 可能是自动加了锁
加锁状态
- import time
- import threading
- def addNum(i):
- global num #在每个线程中都获取这个全局变量
- print("---get num:",i,num)
- time.sleep(1)
- lock.acquire() #修改数据前加锁
- num -= 1 #对此公共变量进行 - 1 操作
- lock.release() # 修改后释放
- num = 100
- thread_list = []
- lock = threading.Lock() #生成全局锁
- for i in range(100):
- t = threading.Thread(target=addNum,args=(i,))
- t.start()
- thread_list.append(t)
- for t in thread_list:
- t.join()
- print('final num:',num)
加锁
GIL VS Lock
机智的同学可能会问到这个问题, 就是既然你之前说过了, Python 已经有一个 GIL 来保证同一时间只能有一个线程来执行了, 为什么这里还需要 lock? 注意啦, 这里的 lock 是用户级的 lock, 跟那个 GIL 没关系
那你又问了, 既然用户程序已经自己有锁了, 那为什么 C python 还需要 GIL 呢? 加入 GIL 主要的原因是为了降低程序的开发的复杂度, 比如现在的你写 python 不需要关心内存回收的问题, 因为 Python 解释器帮你自动定期进行内存回收, 你可以理解为 python 解释器里有一个独立的线程, 每过一段时间它起 wake up 做一次全局轮询看看哪些内存数据是可以被清空的, 此时你自己的程序里的线程和 py 解释器自己的线程是并发运行的, 假设你的线程删除了一个变量, py 解释器的垃圾回收线程在清空这个变量的过程中的 clearing 时刻, 可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了, 结果就有可能新赋值的数据被删除了, 为了解决类似的问题, python 解释器简单粗暴的加了锁, 即当一个线程运行时, 其它人都不能动, 这样就解决了上述的问题, 这可以说是 Python 早期版本的遗留问题.
python 学习笔记之线程, 进程和协程(第七天)
来源: http://www.bubuko.com/infodetail-2714797.html