一, 线程概论
1, 何为线程
每个进程有一个地址空间, 而且默认就有一个控制线程. 如果把一个进程比喻为一个车间的工作过程那么线程就是车间里的一个一个流水线.
进程只是用来把资源集中到一起(进程只是一个资源单位, 或者说资源集合), 而线程才是 CPU 上的执行单位.
多线程 (即多个控制线程) 的概念是, 在一个进程中存在多个控制线程, 多个控制线程共享该进程的地址空间(资源)
创建进程的开销要远大于线程开进程相当于建一个车间, 而开线程相当于建一条流水线.
2, 线程和进程的区别
- Threads share the address space of the process that created it; processes have their own address space.
- Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
- Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
- New threads are easily created; new processes require duplication of the parent process.
- Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
- Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
中译:
, 线程共享创建它的进程的地址空间; 进程有自己的地址空间.
, 线程可以直接访问其进程的数据段; 进程有它们自己的父进程数据段的副本.
, 线程可以直接与进程的其他线程通信; 进程必须使用进程间通信来与兄弟进程通信.
, 新线程很容易创建; 新进程需要复制父进程.
, 线程可以对同一进程的线程进行相当大的控制; 进程只能对子进程执行控制.
, 对主线程的更改 (取消, 优先级更改等) 可能会影响该进程的其他线程的行为; 对父进程的更改不会影响子进程.
3, 多线程的优点
多线程和多进程相同指的是, 在一个进程中开启多个线程
1)多线程共享一个进程的地址空间(资源)
2) 线程比进程更轻量级, 线程比进程更容易创建可撤销, 在许多操作系统中, 创建一个线程比创建一个进程要快 10-100 倍, 在有大量线程需要动态和快速修改时, 这一特性很有用
3) 若多个线程都是 CPU 密集型的, 那么并不能获得性能上的增强, 但是如果存在大量的计算和大量的 I/O 处理, 拥有多个线程允许这些活动彼此重叠运行, 从而会加快程序执行的速度.
4) 在多 CPU 系统中, 为了最大限度的利用多核, 可以开启多个线程, 比开进程开销要小的多.(这一条并不适用于 python)
二, python 的并发编程之多线程
1,threading 模块介绍
multiprocessing 模块的完全模仿了 threading 模块的接口, 二者在使用层面, 有很大的相似性, 因而不再详细介绍
对 multiprocessing 模块也不是很熟悉的朋友可以复习一下多线程时介绍的随笔:
30, 进程的基础理论, 并发(multiprocessing 模块): http://www.cnblogs.com/liluning/p/7419677.html
官方文档:(英语好的可以尝试挑战)
2, 开启线程的两种方式(和进程一模一样)
两种方式里我们都有开启进程的方式可以简单复习回顾
1)方式一:
- from threading import Thread
- #from multiprocessing import Process
- import os
- def talk():
- print('%s is running' %os.getpid())
- if __name__ == '__main__':
- t=Thread(target=talk)
- # t=Process(target=talk)
- t.start()
- print('主',os.getpid())
2)方式二:
- # 开启线程
- from threading import Thread
- import os
- class MyThread(Thread):
- def __init__(self,name):
- super().__init__()
- self.name=name
- def run(self):
- print('pid:%s name:[%s]is running' %(os.getpid(),self.name))
- if __name__ == '__main__':
- t=MyThread('lln')
- t.start()
- print('主 T',os.getpid())
- # 开启进程
- from multiprocessing import Process
- import os
- class MyProcess(Process):
- def __init__(self,name):
- super().__init__()
- self.name=name
- def run(self):
- print('pid:%s name:[%s]is running' % (os.getpid(), self.name))
- if __name__ == '__main__':
- t=MyProcess('lll')
- t.start()
- print('主 P',os.getpid())
3, 在一个进程下开启多个线程与在一个进程下开启多个子进程的区别
1)比较速度:(看看 hello 和主线程 / 主进程的打印速度)
- from threading import Thread
- from multiprocessing import Process
- import os
- def work():
- print('hello')
- if __name__ == '__main__':
- #在主进程下开启线程
- t=Thread(target=work)
- t.start()
- print('主线程 / 主进程')
- #在主进程下开启子进程
- t=Process(target=work)
- t.start()
- print('主线程 / 主进程')
2)pid 的区别:(线程和主进程相同, 子进程和主进程不同)
- from threading import Thread
- from multiprocessing import Process
- import os
- def work():
- print('我的 pid:',os.getpid())
- if __name__ == '__main__':
- #part1: 在主进程下开启多个线程, 每个线程都跟主进程的 pid 一样
- t1=Thread(target=work)
- t2=Thread(target=work)
- t1.start()
- t2.start()
- print('主线程 / 主进程 pid:',os.getpid())
- #part2: 开多个进程, 每个进程都有不同的 pid
- p1=Process(target=work)
- p2=Process(target=work)
- p1.start()
- p2.start()
- print('主线程 / 主进程 pid:',os.getpid())
3)数据是否共享(线程与主进程共享数据, 子进程只是将主进程拷贝过去操作的并非同一份数据)
- from threading import Thread
- from multiprocessing import Process
- def work():
- global n
- n -= 1
- n = 100 #主进程数据
- if __name__ == '__main__':
- # p=Process(target=work)
- # p.start()
- # p.join()
- # print('主',n) #毫无疑问子进程 p 已经将自己的全局的 n 改成了 99, 但改的仅仅是它自己的, 查看父进程的 n 仍然为 100
- t=Thread(target=work)
- t.start()
- t.join()
- print('主',n) #查看结果为 99, 因为同一进程内的线程之间共享进程内的数据
4, 练习
1)三个任务, 一个接收用户输入, 一个将用户输入的内容格式化成大写, 一个将格式化后的结果存入文件
- from threading import Thread
- msg = []
- msg_fort = []
- def Inp():
- while True :
- msg_l = input('>>:')
- if not msg_l : continue
- msg.append(msg_l)
- def Fort():
- while True :
- if msg :
- res = msg.pop()
- msg_fort.append(res.upper())
- def Save():
- with open('db.txt','a') as f :
- while True :
- if msg_fort :
- f.write('%s\n' %msg_fort.pop())
- f.flush() #强制将缓冲区中的数据发送出去, 不必等到缓冲区满
- if __name__ == '__main__':
- p1 = Thread(target=Inp)
- p2 = Thread(target=Fort)
- p3 = Thread(target=Save)
- p1.start()
- p2.start()
- p3.start()
2)将前面随笔中的服务端客户端例子用多线程实现(不了解的可以翻阅前几篇随笔)
服务端
客户端
5,threading 模块其他方法
Thread 实例对象的方法
- # isAlive(): 返回线程是否活动的.
- # getName(): 返回线程名.
- # setName(): 设置线程名.
threading 模块提供的一些方法:
- # threading.currentThread(): 返回当前的线程变量.
- # threading.enumerate(): 返回一个包含正在运行的线程的 list. 正在运行指线程启动后, 结束前, 不包括启动前和终止后的线程.
- # threading.activeCount(): 返回正在运行的线程数量, 与 len(threading.enumerate())有相同的结果.
测试
主线程等其它线程
- from threading import Thread,currentThread,activeCount
- import os,time,threading
- def talk():
- time.sleep(2)
- print('%s is running' %currentThread().getName())
- if __name__ == '__main__':
- t=Thread(target=talk)
- t.start()
- t.join()
- print('主')
6, 守护线程
1)守护线程和守护进程的区别
对主进程来说, 运行完毕指的是主进程代码运行完毕
对主线程来说, 运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕, 主线程才算运行完毕
2)详细说明
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收), 然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程), 才会结束
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收). 因为主线程的结束意味着进程的结束, 进程整体的资源都将被回收, 而进程必须保证非守护线程都运行完毕后才能结束.
守护线程
迷惑人的例子
三, Python GIL(Global Interpreter Lock)
1, 定义:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
在 CPython 中, 全局解释器锁是一个互斥锁, 或 GIL, 它可以防止多个本地线程执行 Python 字节码.
This lock is necessary mainly because CPython's memory management is not thread-safe.
这个锁是必需的, 主要是因为 CPython 的内存管理不是线程安全的.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
然而, 由于 GIL 存在, 其他的特性已经发展到依赖于它的保证.
结论: 在 Cpython 解释器中, 同一个进程下开启的多线程, 同一时刻只能有一个线程执行, 无法利用多核优势
注意:
首先需要明确的一点是 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
对自己英语水平有信心的可以看一下: http://www.dabeaz.com/python/UnderstandingGIL.pdf http://www.dabeaz.com/python/UnderstandingGIL.pdf (这篇文章透彻的剖析了 GIL 对 python 多线程的影响)
2,GIL 介绍
GIL 本质就是一把互斥锁, 既然是互斥锁, 所有互斥锁的本质都一样, 都是将并发运行变成串行, 以此来控制同一时间内共享数据只能被一个任务所修改, 进而保证数据安全.
可以肯定的一点是: 保护不同的数据的安全, 就应该加不同的锁.
要想了解 GIL, 首先确定一点: 每次执行 python 程序, 都会产生一个独立的进程. 例如 python test.py,python aaa.py,python bbb.py 会产生 3 个不同的 python 进程
'''
# 验证 python test.py 只会产生一个进程
#test.py 内容
import os,time
print(os.getpid())
time.sleep(1000)
'''
python3 test.py
- # 在 Windows 下
- tasklist |findstr python
- # 在 Linux 下
- ps aux |grep python
验证 python test.py 只会产生一个进程
在一个 python 的进程内, 不仅有 test.py 的主线程或者由该主线程开启的其他线程, 还有解释器开启的垃圾回收等解释器级别的线程, 总之, 所有线程都运行在这一个进程内, 毫无疑问:
- #1 所有数据都是共享的, 这其中, 代码作为一种数据也是被所有线程共享的(test.py 的所有代码以及 Cpython 解释器的所有代码)
- #2 所有线程的任务, 都需要将任务的代码当做参数传给解释器的代码去执行, 即所有的线程要想运行自己的任务, 首先需要解决的是能够访问到解释器的代码.
综上:
如果多个线程的 target=work, 那么执行流程是多个线程先访问到解释器的代码, 即拿到执行权限, 然后将 target 的代码交给解释器的代码去执行
GIL 保护的是解释器级的数据, 保护用户自己的数据则需要自己加锁处理
保护自己的数据还是需要自己加锁
3,GIL 与多线程
有了 GIL 的存在, 同一时刻同一进程中只有一个线程被执行
听到这里, 你是否会有疑问: 进程可以利用多核, 但是开销大, 而 python 的多线程开销小, 但却无法利用多核优势, 也就是说 python 没用了
要解决这个问题, 我们需要在几个点上达成一致:
- #1. CPU 到底是用来做计算的, 还是用来做 I/O 的?
- #2. 多 CPU, 意味着可以有多个核并行完成计算, 所以多核提升的是计算性能
- #3. 每个 CPU 一旦遇到 I/O 阻塞, 仍然需要等待, 所以多核对 I/O 操作没什么用处
结论:
对计算来说, CPU 越多越好, 但是对于 I/O 来说, 再多的 CPU 也没用
当然对运行一个程序来说, 随着 CPU 的增多执行效率肯定会有所提高(不管提高幅度多大, 总会有所提高), 这是因为一个程序基本上不会是纯计算或者纯 I/O, 所以我们只能相对的去看一个程序到底是计算密集型还是 I/O 密集型, 从而进一步分析 python 的多线程到底有无用武之地
# 分析:
我们有四个任务需要处理, 处理方式肯定是要玩出并发的效果, 解决方案可以是:
方案一: 开启四个进程
方案二: 一个进程下, 开启四个线程
# 单核情况下, 分析结果:
如果四个任务是计算密集型, 没有多核来并行计算, 方案一徒增了创建进程的开销, 方案二胜
如果四个任务是 I/O 密集型, 方案一创建进程的开销大, 且进程的切换速度远不如线程, 方案二胜
# 多核情况下, 分析结果:
如果四个任务是计算密集型, 多核意味着并行计算, 在 python 中一个进程中同一时刻只有一个线程执行用不上多核, 方案一胜
如果四个任务是 I/O 密集型, 再多的核也解决不了 I/O 问题, 方案二胜
# 结论:
现在的计算机基本上都是多核, python 对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升, 甚至不如串行(没有大量切换), 但是, 对于 IO 密集型的任务效率还是有显著提升的.
4, 性能测试
计算密集型: 多进程效率高
I/O 密集型: 多线程效率高
总结:
多线程用于 IO 密集型, 如 socket, 爬虫, web
多进程用于计算密集型, 如金融分析
来源: http://www.bubuko.com/infodetail-2927457.html