并发编程之多线程
线程
- 一个程序的运行过程是一个进程,每个进程自带一个控制线程。
- 进程是一个资源单位,线程是具体的执行单位,即一个进程内可有多个线程。
- 多线程(多个控制线程):在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
# 进程和线程
进程: 资源单位(起一个进程是在内存空间中开辟一块独立的空间)
线程: 执行单位(真正被干活的是进程里面的线程,线程在执行中所需要使用到的资源都找所在的进程索要)
# 程和线程都是虚拟单位,只是为了我们更加方便的描述问题
进程线程比较
- 开进程:申请内存空间,资源消耗大
- 开线程:进程内开多个线程,无需再次申请内存空间操作,消耗小
- 开线程的开销要远远的小于进程的开销。同一个进程下的多个线程数据是共享的
- 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.
为何要用多线程
如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。
#1 多线程共享一个进程的地址空间
#2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在有大量线程需要动态和快速修改时,这一特性很有用。
#3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
#4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。
# but, python的多线程无法使用多核优势
开启线程的两种方式
开启进程的方式和开启线程的方式一模一样,只是使用的模块不一样。
总结:
- 开线程的开销非常下,几乎是代码执行就执行的线程的任务。
- 开线程可以不写在main判断下。
from threading import Thread
def task():
print('i am sub_thread')
# 方式1:直接使用Thread实例化线程对象
if __name__ == '__main__':
t = Thread(target=task)
t.start()
print('i am main-thread')
# 方式2:继承Therad,自定自己的线程类,重写run方法
class MyThread(Thread):
def run(self):
task()
if __name__ == '__main__':
t = MyThread()
t.start()
print('i am main-thread')
实例:TCP服务端多线程实现并发
import socket
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 线程的任务:通信循环
def task(conn):
while 1:
try:
data = conn.recv(1024)
if not data: break
conn.send(data.upper())
except Exception as e:
print(e)
break
conn.close()
while 1:
conn, addr = server.accept()
t = Thread(target=task, args=(conn, )) # 来一个连接请求,就开一个线程处理这个连接
t.start() # 这种并发方式有缺陷:容易内存溢出(线程个数有限)
线程的join方法
线程对象的join方法,就是让主线程等待子线程,子线程运行结束后再执行主线程。
if __name__ == '__main__':
t = MyThread()
t.start()
t.join() # 主线程等待子线程运行结束再执行
print('i am main-thread')
多线程间共享数据
同一个进程下的多个线程是共享当前进程下的数据资源的
from threading import Thread
n = 100
def func():
global n
n = 110
print('sub_thread', n) # 110
if __name__ == '__main__':
t = Thread(target=func)
t.start()
# t.join()
print('main-thread', n) # 110 两次输出结果一致,都是访问的一个数据
线程相关其他方法
基本和进程对象的差不多,不再赘述。
# 线程对象的
# native_id 获取当前对象的线程id
from threading import Thread, active_count, current_thread
# sctive_count() 获取当前存活的线程数
# current_thread() 获取当前线程对象,
# current_thread().name 当前对象的没名字等
# 补充:
active_count 等同于 currentThread,
current_thread 等同于 activeCount
守护线程
守护线程:设置子线程为守护线程,当主线程(即程序)运行结束时,守护线程随着主线程的结束而结束。
总结:
-
主线程,就是当前程序,程序一旦运行到最后一行代码就结束。
-
如果子线程不是守护线程,程序运到最后一行代码时会在原地等待子线程的。
-
如果主线程结束了,那么守护线程会随之结束。
-
因为主线程的结束意味着所在的进程的结束。
# 主线程之所以在程序最后处等待其他非守护线程的结束,是因为主线程就是当前程序,即进程。
# 当前程序如果结束了,那么这些子线程就没办法继续工作。(车间[进程]倒闭了,流水线[线程]就没有意义了)。
线程互斥锁
线程互斥锁和进程互斥锁概念一致,都是将并发变为串行,牺牲效率,保证数据安全。
补充:
- 互斥锁:局部,并发变串行
- start后直接join:整体,并发变串行
from threading import Thread,Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1) # 模拟网络延迟
money = tmp - 1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money) # 0, 如果没有锁,那么结果就是99了,大家同时拿到的都是一个数据,100-1=99
死锁和递归锁
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
解决方法:递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
详情:参考博客
GIL
全局解释器锁(GIL, Global Interpreter Lock)。
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
# 程序的代码需要交给解释器执行,因此该进程内的每个线程都需要将共享的代码交给解释器执行,这就有了竞争。
# 垃圾回收机制GC也是当前进程内的一个干活的线程,GC会和当前进程内其他线程有抢代码数据的竞争,因此有了GIL。
# GIL保证进程内同一时刻只有一个线程在运行,这样做是为了保证内存管理(垃圾回收机制GC)的运行。
总结:
-
GIL不是python语言的特性,它是Cpython解释器引用的一个概念。
-
GIL本质是一把互斥锁,是将线程从并发变为串行,牺牲效率保证数据安全。
-
保护不同的数据要加不同的互斥锁,GIL保护的是解释器级别的数据安全。
初步图解,思考的不深,待日后补充。
python多线程的应用
首先明确:python多线程无法利用计算机的多核优势。
但是python的所线程可以实现并发的,且不是所有的场合都要使用多核的。
场合:
- 对于计算密集型,多进程优于多线程(多进程的并行计算优势突出了)
- 对于IO密集型,多线程优于多进程(开进程的开销大缺陷放大了)
结论:python多线程无法使用多核优势,不等于多线程一无是处。