THU.线程
线程
进程
- 进程是操作系统分配资源的最小单位,一个进程至少有一个主线程,一个进程中可以开启多个线程
有了进程为什么要有线程
进程的缺陷
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了
- 进程在执行的过程中如果阻塞,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也无法执行
- 占用资源更多
线程就是将一个进程拆分成一个个可以独立运行的基本单位,进而提高运行效率。
# 进程是资源分配的最小单位,线程是 CPU 调度的最小单位
进程和线程的区别
- 进程间相互独立,统一进程的各线程间共享资源
- 线程可以直接读写进程数据段来进行通信,但需要进程同步和互斥手段的辅助,以保证数据的一致性
- 线程所占用的资源相对于进程更少,因此调度和切换更快
- 在多线程操作系统中,进程不是一个可执行的实体
- 同一进程中的线程,无法利用多核优势
线程的特点
-
在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用 CPU 的基本单位,是占用资源最小的实体
-
轻型实体
- 线程中的实体几乎不拥有系统资源,仅有一点必不可少的、能保证独立运行的资源
-
独立调度和分派的基本单位
- 在多线程中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位
-
共享进程资源
- 线程在统一进程中的各个线程,都可以共享该进程所拥有的资源,且不必调用内核
-
可并发执行
- 在一个进程中的多个线程之间,可以并发执行;同样。不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力
-
GIL 全局解释器锁
-
Python 代码的执行由 Python 解释器来控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然在 Python解释器中可以“运行”多个线程,但任意时刻只有一个线程在解释器中运行。
-
对 Python 解释器的访问由全局解释器锁(GIL)来控制,正是这个锁能保证给同一时刻只有一个线程在运行
-
在 Python 解释器中,只在 cPython 中有,pypy 解释器是没有 GIL 锁的,因此 pypy 解释器更快
-
启动一个垃圾回收线程,一个是正常执行的线程
-
是这里一把(GIL 锁),一个先线程要向执行,必须拿到这把锁
-
同一时刻,同一进程,可以有多个线程,但只有一个线程在执行
-
如果是计算密集型(主要靠 CPU,可以利用多核优势),要开进程
-
如果是 IO 密集型,要开线程
多线程与多进程
pid的比较
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello',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())
开启效率的较量
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello')
if __name__ == '__main__':
# 在主进程下开启线程
t=Thread(target=work)
t.start()
print('主线程/主进程')
'''
打印结果:
hello
主线程/主进程
'''
# 在主进程下开启子进程
t=Process(target=work)
t.start()
print('主线程/主进程')
'''
打印结果:
主线程/主进程
hello
'''
内存数据的共享问题
from threading import Thread
from multiprocessing import Process
import os
def work():
global n
n=0
if __name__ == '__main__':
# n=100
# p=Process(target=work)
# p.start()
# p.join()
# print('主',n) # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
n=1
t=Thread(target=work)
t.start()
t.join()
print('主',n) # 查看结果为0,因为同一进程内的线程之间共享进程内的数据
互斥锁(同步锁)
- 用以保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
import time
from threading import Thread
def minus():
global n
temp = n
time.sleep(0.1)
n = temp - 1
if __name__ == '__main__':
n = 10
l = []
for i in range(10):
t = Thread(target=minus, args=(lock,))
t.start()
l.append(t)
for j in l:
j.join()
print("主线程>>>: %s" % n) # 由于 time 模块的暂停功能,导致子进程并未完成对 n 的数据操作过程,而主进程仍然在循环执行,以致所有子进程的 n 取到的值均为 10,因此输出结果为 9。
- 并发在高效运行的同时会影响数据安全,因此需要增加互斥锁来保证数据安全
import time
from threading import Thread, Lock
def minus(lock):
global n
lock.acquire()
temp = n
time.sleep(0.1)
n = temp - 1
lock.release()
if __name__ == '__main__':
lock = Lock()
n = 10
l = []
for i in range(10):
t = Thread(target=minus, args=(lock,))
t.start()
l.append(t)
for j in l:
j.join()
print("主线程>>>: %s" % n) # 输出结果为 0
互斥锁与 join 的区别
信号量(了解)
- 信号量(Semaphore),信号量和锁相似,锁同一时间只允许一个对象(进程)通过,信号量同一时间允许多个对象(进程)通过
import time
from threading import Thread, Semaphore
def task(i):
sm.acquire()
print("第 %s 辆车进入停车场" % i)
time.sleep(2)
print("第 %s 辆车离开停车场" % i)
sm.release()
if __name__ == '__main__':
print("-----这里有两个停车位-----")
sm = Semaphore(2)
for i in range(1, 5):
t = Thread(target=task, args=(i,))
t.start()
Event 事件
- Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。
- 如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
- 一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
- 如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
import time
from threading import Event, Thread
def ParkingLot(event):
print("已无空闲车位")
time.sleep(3)
print("有空闲车位了")
event.set()
def car(event, i):
print("第 %s 辆车正在等待车位" % i)
event.wait()
print("第 %s 辆车正在停车" % i)
if __name__ == '__main__':
event = Event()
t1 = Thread(target=ParkingLot, args=(event,))
t1.start()
for i in range(1, 10):
t2 = Thread(target=car, args=(event, i))
t2.start()