简介
- 多线程,就类似与操作系统中的多进程。简单的讲,就是可以同时并发执行多个任务,处理多件事情
- 线程是最小的执行单元,而进程由至少一个线程组成
- 主线程会等待所有的子线程结束后才结束
创建线程
使用threading模块的Thread类,通过指定target与args(可选)参数。
通过继承Thread类,重写run方法。
方式一:使用threading.Thread
from threading import Thread
from time import sleep, ctime
def sing():
for i in range(3):
print('正在唱歌%d:%s' % (i, ctime()))
sleep(1)
def dance():
for i in range(3):
print('正在跳舞%d:%s' % (i, ctime()))
sleep(1)
if __name__ == '__main__':
t1 = Thread(target=sing)
t2 = Thread(target=dance)
t1.start()
t2.start()
输出信息:
正在唱歌0:Thu Jul 26 11:21:33 2018
正在跳舞0:Thu Jul 26 11:21:33 2018
正在唱歌1:Thu Jul 26 11:21:34 2018
正在跳舞1:Thu Jul 26 11:21:34 2018
正在唱歌2:Thu Jul 26 11:21:35 2018
正在跳舞2:Thu Jul 26 11:21:35 2018
方式二:继承Thread
from threading import Thread
from time import sleep, ctime
class MyThread1(Thread):
def run(self):
for i in range(3):
print('%s:%d:%s' % (self.name, i, ctime()))
sleep(1)
class MyThread2(Thread):
def run(self):
for i in range(3):
print('%s:%d:%s' % (self.name, i, ctime()))
sleep(1)
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
输出信息:
Thread-1:0:Thu Jul 26 11:24:22 2018
Thread-2:0:Thu Jul 26 11:24:22 2018
Thread-1:1:Thu Jul 26 11:24:23 2018
Thread-2:1:Thu Jul 26 11:24:23 2018
Thread-1:2:Thu Jul 26 11:24:24 2018
Thread-2:2:Thu Jul 26 11:24:24 2018
线程的生命周期
- 新建:创建对象
- 就绪:调用start后
- 运行:获得CPU资源
- 阻塞(挂起):失去CPU资源
- 死亡:线程执行结束或抛出未捕获的异常
线程相关操作
threading模块相关功能:
- threading.active_count() : 当前活动线程数量
- theading.enumerate() : 获取所有活跃线程
- theading.current_thread() : 获取当前运行线程
- threading.get_ident() : 获取线程标识
- threading.main_thread() : 获取主线程
线程对象相关功能:
- start():启动线程
- run():运行线程
- join(timeout=None) : 阻塞调用线程,直到队列中的所有任务被处理掉
- is_alive():是否活动
线程锁
互斥锁:当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
- threading.Lock():获得线程锁
- acquire():尝试去获得锁。
- release():释放所获得的锁。
注意:要确保锁得到有效的释放
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
from threading import Thread, Lock
def thread1():
global g_num
for i in range(100000):
# 上锁
# True:表示阻塞,如果这个锁在上锁之前已经上锁,那么这个线程就会在这里一直等待解锁位置
# False:表示非阻塞,即不管本次调用能否成功上锁,都不会卡在这里,而是继续执行下面的代码
mutex_flag = mutex.acquire(True)
if mutex_flag:
g_num += 1
# 释放锁
mutex.release()
print('-- test1 --', g_num)
def thread2():
global g_num
for i in range(100000):
# 上锁
#
mutex_flag = mutex.acquire(True)
if mutex_flag:
g_num += 1
mutex.release()
print('-- test2 --', g_num)
g_num = 0
# 创建一个互斥锁
# 这个锁默认是未上锁的状态
mutex = Lock()
t1 = Thread(target=thread1)
t2 = Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()
print('-- main --', g_num)
输出信息:
-- test2 -- 189183
-- test1 -- 200000
-- main -- 200000
线程同步
多个线程有序执行
from threading import Thread, Lock
from time import sleep
class Task1(Thread):
def run(self):
while True:
if lock1.acquire():
print('-- Task 1 --')
sleep(0.5)
lock2.release()
class Task2(Thread):
def run(self):
while True:
if lock2.acquire():
print('-- Task 2 --')
sleep(0.5)
lock3.release()
class Task3(Thread):
def run(self):
while True:
if lock3.acquire():
print('-- Task 3 --')
sleep(0.5)
lock1.release()
lock1 = Lock()
lock2 = Lock()
lock3 = Lock()
lock2.acquire()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
输出信息:
-- Task 1 --
-- Task 2 --
-- Task 3 --
-- Task 1 --
-- Task 2 --
-- Task 3 --
-- Task 1 --
-- Task 2 --
...
ThreadLocal
- 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
- 一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁
import threading
# 创建全局ThreadLocal对象
local_school = threading.local()
def process_student():
# 获取当前线程管理的student
std = local_school.student
print('%s(in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student
local_school.student = name
process_student()
if __name__ == '__main__':
t1 = threading.Thread(target=process_thread, args=('Tom',), name='Thread_A')
t2 = threading.Thread(target=process_thread, args=('Jim',), name='Thread_B')
t1.start()
t2.start()
输出信息:
Tom(in Thread_A)
Jim(in Thread_B)
Condition (条件变量)
Condition(条件变量)通常与一个锁关联。需要在多个Contidion中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。
可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于等待阻塞状态,直到另一个线程调用notify()/notifyAll()通知;得到通知后线程进入锁定池等待锁定。
- acquire([timeout])/release():调用关联的锁的相应方法。
- wait([timeout]):调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用当前线程必须已获得锁定,否则将抛出异常。
- notify():调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
- notifyAll():调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
import threading
import time
# 商品
product = None
# 条件变量
con = threading.Condition()
# 生产者
def produce():
global product
# 开启锁
if con.acquire():
while True:
if product is None:
print('produce...')
product = '商品'
# 通知消费者,商品已经生产
# 收到通知的线程将自动调用acquire()尝试获得锁定
con.notify()
# 等待通知
# 线程进入Condition的等待池等待通知,并释放锁
con.wait()
time.sleep(1)
# 消费者
def consume():
global product
if con.acquire():
while True:
if product is not None:
print('consume...')
product = None
# 通知生产者,商品没了
con.notify()
# 等待通知
con.wait()
time.sleep(1)
t1 = threading.Thread(target=produce)
t2 = threading.Thread(target=consume)
t1.start()
t2.start()
输出信息:
produce...
consume...
produce...
consume...
...
线程队列
Python的Queue模块中提供了同步的、线程安全的队列类
queue模块提供了队列的功能,该模块具有三个类:
- queue.Queue(maxsize=0):FIFO(先入先出)队列Queue
- queue.LifoQueue(maxsize=0):LIFO(后入先出)队列LifoQueue
- queue.PriorityQueue(maxsize=0):优先级队列PriorityQueue
这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
from queue import Queue
import threading
import time
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
# 当商品数量低于1000时,生产者生产100个
if queue.qsize() < 1000:
for i in range(100):
count = count + 1
msg = '生产商品' + str(count)
print(msg)
queue.put(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
# 当商品数量多于100个时,消费者消费3个
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消费了' + queue.get()
print(msg)
time.sleep(1)
if __name__ == '__main__':
queue = Queue()
for i in range(500):
queue.put('初始产品' + str(i))
# 开启2位生产者
for i in range(2):
p = Producer()
p.start()
# 开启5位消费者
for i in range(5):
c = Consumer()
c.start()
GIL (global interpreter lock)
全局解释器锁,是在解释器层面上进行的线程同步
cpython多线程执行的效率往往不够理想,受到GIL锁的影响 同一时间内,只有一个线程对python对象进行操作,基本有点接近单线程 有很多人都想进城门,每一次只能有一个人进,甲进门,其他都不能进门 这样设计的目的主要是为了安全性,cpython内存管理是非线程安全的 python代码的执行是由python虚拟机(解释器主循环)来控制的,在主循环中同一时间内只能有一个线程来执行
python虚拟机如何来执行代码
- 要获取 GIL
- 切换到当前线程去执行python代码
- 1 先执行当前线程中的代码
- 2 当前线程代码执行完毕,主动让出控制权
- 当前让出了控制权,状态处于休眠或者等待
- GIL解锁
- 重复上述的过程
并发执行,并行执行
- 并发执行:多个线程之间来回切换—>并不是同一时间同时在执行多个任务
- 并行执行:多线程同一时间内同时运行 百米赛跑:所有运动员都开始奔跑
死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name + '--do1 up--')
time.sleep(1)
if mutexB.acquire():
print(self.name + '--do1 down--')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
if mutexB.acquire():
print(self.name + '--do2 up--')
time.sleep(1)
if mutexA.acquire():
print(self.name + '--do2 down--')
mutexA.release()
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
并行与并发
- 并发:看上去同时执行,任务数多于核心数
- 并行:真正同时执行,任务数小于等于核心数
线程和进程的对比
定义不同
- 进程是系统进行资源分配和调度的一个独立单位
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程 自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但 是它可与同属一个个进程的其他的线程共享进程所拥有的全部资源.
区别
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高
- 进程在执⾏过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了 程序的运行效率
- 线线程不能够独立执行,必须依存在进程中
优缺点
- 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和 保护;而进程正相反