本文是基于 Py2.X
线程
多任务可以由多进程完成, 也可以由一个进程内的多线程完成
我们前面提到了进程是由若干线程组成的, 一个进程至少有一个线程
多线程类似于同时执行多个不同程序, 多线程运行有如下优点:
可以把运行时间长的任务放到后台去处理
用户界面可以更加吸引人, 比如用户点击了一个按钮去触发某些事件的处理, 可以弹出一个进度条来显示处理的进度
程序的运行速度可能加快
在一些需要等待的任务实现上, 如用户输人文件读写和网络收发数据等, 线程就比较有用了在这种情况下我们可以释放一些珍贵的资源, 如内存占用等
Python 的标准库提供了两个模块: thread 和 threading,thread 是低级模块, threading 是高级模块, 对 thread 进行了封装绝大多数情况下, 我们只需要使用 threading 这个高级模块
启动一个线程就是把一个函数传入并创建 Thread 实例, 然后调用 start() 开始执行:
- # -*- coding:utf-8 -*-
- 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 >>> 1
- thread LoopThread >>> 2
- thread LoopThread >>> 3
- thread LoopThread >>> 4
- thread LoopThread >>> 5
- thread LoopThread ended.
- thread MainThread ended.
由于任何进程默认就会启动一个线程, 我们把该线程称为主线程, 主线程又可以启动新的线程, Python 的 threading 模块有个 current_thread() 函数, 它永远返回当前线程的实例主线程实例的名字叫 MainThread, 子线程的名字在创建时指定, 我们用 LoopThread 命名子线程名字仅仅在打印时用来显示, 完全没有其他意义, 如果不起名字 Python 就自动给线程命名为 Thread-1,Thread-2
Lock
多线程和多进程最大的不同在于, 多进程中, 同一个变量, 各自有一份拷贝存在于每个进程中, 互不影响, 而多线程中, 所有变量都由所有线程共享, 所以, 任何一个变量都可以被任何一个线程修改, 因此, 线程之间共享数据最大的危险在于多个线程同时改一个变量, 把内容给改乱了
- import time, threading
- # 假定这是你的银行存款:
- balance = 0
- def change_it(n):
- # 先存后取, 结果应该为 0:
- global balance
- balance = balance + n
- balance = balance - n
- def 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
得到:
46,
且每次运行结果都会不一样
我们定义了一个共享变量 balance, 初始值为 0, 并且启动两个线程, 先存后取, 理论上结果应该为 0, 但是, 由于线程的调度是由操作系统决定的, 当 t1t2 交替执行时, 只要循环次数足够多, balance 的结果就不一定是 0 了
由于彼此间的交替运算, 所以结果会发生变化, 如果是在银行操作, 一存一取就可能导致余额不对, 所以必须确保一个线程在修改 balance 的时候, 别的线程一定不能改
如果我们要确保 balance 计算正确, 就要给 change_it() 上一把锁, 当某个线程开始执行 change_it() 时, 我们说, 该线程因为获得了锁, 因此其他线程不能同时执行 change_it(), 只能等待, 直到锁被释放后, 获得该锁以后才能改由于锁只有一个, 无论多少线程, 同一时刻最多只有一个线程持有该锁, 所以, 不会造成修改的冲突创建一个锁就是通过 threading.Lock() 来实现:
修改后的代码:
- # -*- coding:utf-8 -*-
- import time, threading
- # 假定这是你的银行存款:
- balance = 0
- def change_it(n):
- # 先存后取, 结果应该为 0:
- global balance
- balance = balance + n
- balance = balance - n
- lock = threading.Lock()
- def run_thread(n):
- for i in range(100000):
- # 先要获取锁:
- lock.acquire()
- try:
- # 放心地改吧:
- change_it(n)
- finally:
- # 改完了一定要释放锁:
- lock.release()
- 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
结果, 无论怎么执行都是 0, 这正是我们期望的结果
当多个线程同时执行 lock.acquire() 时, 只有一个线程能成功地获取锁, 然后继续执行代码, 其他线程就继续等待直到获得锁为止
获得锁的线程用完后一定要释放锁, 否则那些苦苦等待锁的线程将永远等待下去, 成为死线程所以我们用 try...finally 来确保锁一定会被释放
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行, 坏处当然也很多, 首先是阻止了多线程并发执行, 包含锁的某段代码实际上只能以单线程模式执行, 效率就大大地下降了其次, 由于可以存在多个锁, 不同的线程持有不同的锁, 并试图获取对方持有的锁时, 可能会造成死锁, 导致多个线程全部挂起, 既不能执行, 也无法结束, 只能靠操作系统强制终止
全局解释器
如果你不幸拥有一个多核 CPU, 你肯定在想, 多核应该可以同时执行多个线程
在 Python 的原始解释器 CPython 中存在着 GIL(Global Interpreter Lock, 全局解释器锁) 因此在解释执行 Python 代码时, 会产生互斥锁来限制线程对共享资源的访问, 直到解释器遇到 I/O 操作或者操作次数达到一定数据时支委会释放 GIL 由于全局器锁的存在, 在进行多线程操作的时候, 不能调用多个 CPU 内核, 只能利用一个内核, 所以在进行 CPU 密集型操作的时候, 不推荐使用多线程, 更加倾向于多进程, 那么多线程适合什么样的应用场景呢? 对于 IO 密集型操作, 多线程可以明显提高效率, 例如 Python 爬虫的开发, 绝大多数时间爬虫是在等待 socket 返回数据, 网络 IO 操作延时比 CPU 大得多
ThreadLocal
在多线程环境下, 每个线程都有自己的数据一个线程使用自己的局部变量比使用全局变量好, 因为局部变量只有线程自己能看见, 不会影响其他线程, 而全局变量的修改必须加锁
但是局部变量也有问题, 就是在函数调用的时候, 传递起来很麻烦:
- def process_student(name):
- std = Student(name)
- # std 是局部变量, 但是每个函数都要用它, 因此必须传进去:
- do_task_1(std)
- do_task_2(std)
- def do_task_1(std):
- do_subtask_1(std)
- do_subtask_2(std)
- def do_task_2(std):
- do_subtask_2(std)
- do_subtask_2(std)
每个函数一层一层调用都这么传参数那还得了? 用全局变量? 也不行, 因为每个线程处理不同的 Student 对象, 不能共享
如果用一个全局 dict 存放所有的 Student 对象, 然后以 thread 自身作为 key 获得线程对应的 Student 对象如何?
- global_dict = {}
- def std_thread(name):
- std = Student(name)
- # 把 std 放到全局变量 global_dict 中:
- global_dict[threading.current_thread()] = std
- do_task_1()
- do_task_2()
- def do_task_1():
- # 不传入 std, 而是根据当前线程查找:
- std = global_dict[threading.current_thread()]
- ...
- def do_task_2():
- # 任何函数都可以查找出当前线程的 std 变量:
- std = global_dict[threading.current_thread()]
- ...
这种方式理论上是可行的, 它最大的优点是消除了 std 对象在每层函数中的传递问题, 但是, 每个函数获取 std 的代码有点丑
有没有更简单的方式?
ThreadLocal 应运而生, 不用查找 dict,ThreadLocal 帮你自动做这件事:
- import threading
- # 创建全局 ThreadLocal 对象:
- local_school = threading.local()
- def process_student():
- print Hello, %s (in %s) % (local_school.student, threading.current_thread().name)
- def process_thread(name):
- # 绑定 ThreadLocal 的 student:
- local_school.student = name
- process_student()
- t1 = threading.Thread(target= process_thread, args=(Alice,), name=Thread-A)
- t2 = threading.Thread(target= process_thread, args=(Bob,), name=Thread-B)
- t1.start()
- t2.start()
- t1.join()
- t2.join()
得到:
- Hello, Alice (in Thread-A)
- Hello, Bob (in Thread-B)
全局变量 local_school 就是一个 ThreadLocal 对象, 每个 Thread 对它都可以读写 student 属性, 但互不影响你可以把 local_school 看成全局变量, 但每个属性如
local_school.student
都是线程的局部变量, 可以任意读写而互不干扰, 也不用管理锁的问题, ThreadLocal 内部会处理
可以理解为全局变量 local_school 是一个 dict, 不但可以用
local_school.student
, 还可以绑定其他变量, 如
local_school.teacher
等等
ThreadLocal 最常用的地方就是为每个线程绑定一个数据库连接, HTTP 请求, 用户身份信息等, 这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源
来源: http://www.bubuko.com/infodetail-2492455.html