线程
一、线程介绍
什么是线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不独立拥有系统资源,但它可与同属一个进程的其它线程共享该进程所拥有的全部资源。每一个应用程序都至少有一个进程和一个线程。在单个程序中同时运行多个线程完成不同的被划分成一块一块的工作,称为多线程。
线程的特点
线程是一个execution context
(执行上下文),即一个cpu执行时所需要的一串指令。假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时继续先前的进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context
。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。
线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只能干一件事,所谓的多线程和并发处理只是假象。CPU能这样做是因为它有每个任务的execution context
,就像你能够和你朋友共享同一本书一样。
进程与线程区别:
- 同一个进程中的线程共享同一内存空间,但进程之间的内存空间是独立的。
- 同一个进程中的所有线程的数据是共享的,但进程之间的数据是独立的。
- 对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
- 线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
- 同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
- 创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
- 一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
- 线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
二、多线程threading方法
threading模块提供了一些比较实用的方法或者属性, 例如:
方法与属性 | 描述 |
---|---|
current_thread() | 返回当前线程 |
active_count() | 返回当前活跃的线程数,1个主线程+n个子线程 |
get_ident() | 返回当前线程 |
enumerater() | 返回当前活动 Thread 对象列表 |
main_thread() | 返回主 Thread 对象 |
settrace(func) | 为所有线程设置一个 trace 函数 |
setprofile(func) | 为所有线程设置一个 profile 函数 |
stack_size([size]) | 返回新创建线程栈大小;或为后续创建的线程设定栈大小为 size |
TIMEOUT_MAX | Lock.acquire(), RLock.acquire(), Condition.wait() 允许的最大超时时间 |
threading模块包含下面的类:
- Thread:基本线程类
- Lock:互斥锁
- RLock:可重入锁,使单一进程再次获得已持有的锁(递归锁)
- Condition:条件锁,使得一个线程等待另一个线程满足特定条件,比如改变状态或某个值。
- Semaphore:信号锁。为线程间共享的有限资源提供一个”计数器”,如果没有可用资源则会被阻塞。
- Event:事件锁,任意数量的线程等待某个事件的发生,在该事件发生后所有线程被激活
- Timer:一种计时器
- Barrier:Python3.2新增的“阻碍”类,必须达到指定数量的线程后才可以继续执行。
2.1、多线程创建
有两种方式来创建线程:一种是继承Thread类,并重写它的run()方法;另一种是在实例化
threading.Thread
对象的时候,将线程要执行的任务函数作为参数传入线程。
方法一
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 每次都执行了 Mythred 类方法
import threading
class Mythread(threading.Thread):
def __init__(self, thread_name):
super(Mythread, self).__init__()
def run(self):
print("{} 正在运行".format(self.name))
if __name__ == '__main__':
for i in range(10):
Mythread("thread-" + str(i)).start()
方法二
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 竞争式执行
import threading
import time
def show(arg):
time.sleep(1)
print("threading"+ str(arg) + "running....")
for i in range(10):
t = threading.Thread(target=show, args=(i,))
t.start()
对于Thread类,它的定义如下:
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
- 参数group是预留的,用于将来扩展;
- 参数target是一个可调用对象,在线程启动后执行;
- 参数name是线程的名字。默认值为“Thread-N“,N是一个数字。 如 <Thread(Thread-6, started 7944)>
- 参数args和kwargs分别表示调用target时的参数列表和关键字参数。
Thread类定义了以下常用方法与属性:
方法与属性 | 说明 |
---|---|
start() | 启动线程,等待CPU调度 如: t = threading.Thread(target=show, args=(i,)) t.start() |
run() | 线程被cpu调度后自动执行的方法 |
getName()、setName()和name | 用于获取和设置线程的名称。 |
setDaemon() | 设置为后台线程或前台线程(默认是False,前台线程)。 如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止。 如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程执行完成后,程序才停止。 |
ident | 获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。 |
is_alive() | 判断线程是否是激活的(alive)。从调用start()方法启动线程,到run()方法执行完毕或遇到未处理异常而中断这段时间内,线程是激活的。 |
isDaemon()方法和daemon属性 | 是否为守护线程 |
join([timeout]) | 调用该方法将会使主调线程堵塞,直到被调用线程运行结束或超时。参数timeout是一个数值类型,表示超时时间,如果未提供该参数,那么主调线程将一直堵塞到被调线程结束。 |
在多线程执行过程中,有一个特点要注意,那就是每个线程各执行各的任务,不等待其它的线程,自顾自的完成自己的任务,比如下面的例子:
import time
import threading
def doWaiting():
print('start waiting:', time.strftime('%H:%M:%S'))
time.sleep(3)
print('stop waiting', time.strftime('%H:%M:%S'))
t = threading.Thread(target=doWaiting)
t.start()
# 确保线程t已经启动
time.sleep(1)
print('start job')
print('end job')
# 结果 先执行主线程 , 到start waitting 然后继续往下执行, 前台进程到sleep 然后主线程继续执行
start threading 13:22:35
start
end
end threading 13:22:38
setDaemon(True)
把所有的子线程都变成主线程的守护线程,当主线程结束后,守护子线程也会随之结束, 即后台线程
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import time
import threading
def run():
print(threading.current_thread().getName(), "开始工作")
time.sleep(2) # 子线程停2s
print("子线程工作完毕")
for i in range(3):
t = threading.Thread(target=run,)
t.setDaemon(True) # 把子线程设置为守护线程,必须在start()之前设置
t.start()
time.sleep(1) # 主线程停1秒
print("主线程结束了!")
print(threading.active_count()) # 输出活跃的线程数
结果:
Thread-1
Thread-2
Thread-3
主线程结束, # 当for 执行完, 而run的print还在休眠, 前台线程已经执行完了, 后台线程就会随之关闭
4
2.2、自定义线程类
对于threading模块中的Thread类,本质上是执行了它的run方法。因此可以自定义线程类,让它继承Thread类,然后重写run方法。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import threading
class Mythread(threading.Thread):
def __init__(self, func, arg):
super(Mythread, self).__init__()
self.func = func
self.arg = arg
def run(self):
self.func(self.arg)
def my_func(arg):
print("这里是自定义的线程: {0}".format(threading.current_thread()))
obj = Mythread(my_func, arg=(1,2,3))
obj.start()
# 结果: 这里是自定义的线程: <Mythread(Thread-1, started 2496)>
三、线程锁
由于线程之间的任务执行是CPU进行随机调度的,并且每个线程可能只执行了n条指令之后就被切换到别的线程了。当多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,这被称为“线程不安全”。为了保证数据安全,我们设计了线程锁,即同一时刻只允许一个线程操作该数据。
线程锁用于锁定资源,可以同时使用多个锁,当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个箱子锁住是一个道理。
我们先看一下没有锁的情况下,脏数据是如何产生的。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import time
import threading
number=0
def plus():
global number
for _ in range(100000):
number += _
print("子进程结束, 最终结果: {}".format(number))
for i in range(2):
t = threading.Thread(target=plus)
t.start()
time.sleep(2)
print("主线程结束")
# 执行结果
子进程结束, 最终结果: 127174682081 # 由于CPU执行的速度特别快, 10000以下的结果算出还是正确的
子进程结束, 最终结果: 131963815153 # 由于没有锁 CPU在随机调度的时候就会产生数据错误
主线程结束
这是因为两个线程在运行过程中,CPU随机调度,你算一会我算一会,在没有对number进行保护的情况下,就发生了数据错误。如果想获得正确结果,可以使用join()方法,让多线程变成顺序执行,如下修改代码片段:
最简单的办法就是添加 join 但是这就将多线程改为单线程了没有多大的意义,Python在threading模块中定义了几种线程锁类,分别是:
- Lock 互斥锁
- RLock 可重入锁
- Semaphore 信号
- Event 事件
- Condition 条件
- Barrier “阻碍”
3.1、互斥锁Lock
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import threading
import time
number = 0
lock = threading.Lock()
def plus(lk):
global number # global声明此处的number是外面的全局变量number, 不建议这么操作
lk.acquire() # 获取锁
for _ in range(1000000): # 进行一个大数级别的循环加一运算
number += _
print("子线程结束, 结果为: {}".format(number))
lk.release()
if __name__ == '__main__':
for i in range(2): # 用2个子线程,就可以观察到脏数据
t = threading.Thread(target=plus, args=(lock,))
t.start()
time.sleep(2)
print("主线程结束....")
# 结果
子线程结束, 结果为: 499999500000
子线程结束, 结果为: 999999000000
主线程结束....
互斥死锁
3.2、递归锁Rlock
RLock的使用方法和Lock一模一样,只不过它支持重入锁。该锁对象内部维护着一个Lock和一个counter对象。counter对象记录了acquire的次数,使得资源可以被多次require。最后,当所有RLock被release后,其他线程才能获取资源。在同一个线程中,RLock.acquire()
可以被多次调用,利用该特性,可以解决部分死锁问题。
# 同时开启多把锁,但钥匙只有一把,递归解锁
# 对数据有严格的安全性的时候, 就可以使用递归锁或者互斥锁了
import threading
import time
rlock = threading.RLock()
lk1 = rlock
lk2 = rlock
class Mythread(threading.Thread):
# 如果使用互斥锁, 就会造成死锁的现象,
# 当A 线程在执行有B的锁, 然后 B 线程在执行过程有A的锁,没有释放就会造成死锁
# # CPU在线程上下文切换中执行的速度很快, 当执行foo第一把锁时,在次执行bar 而lk1的锁没有被释放, 那么在执行完lk2的线程之后就会死锁, 这时侯就需要使用 递归锁了
def run(self):
self.foo()
self.bar()
def foo(self):
lk1.acquire() # 假设如果是 互斥锁,那么如果 锁2没有释放那么就会造成死锁的现象产生。
print("取到第一把锁, {0}".format(threading.current_thread().getName()))
lk2.acquire()
print("取到第二把锁, {0}".format(threading.current_thread().getName()))
lk2.release() # 取到锁, 然后执行完代码, 立刻释放锁
lk1.release()
def bar(self):
lk2.acquire()
print("B第一把锁 {}".format(self.name))
lk1.acquire()
time.sleep(1)
print("B第二把锁 {}".format(self.name))
lk1.release()
lk2.release()
if __name__ == '__main__':
t1 = Mythread()
t1.start()
3.3、信号Semaphore
类名:BoundedSemaphore。这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import time
import threading
def run(num, lk):
lk.acquire() # 获取锁
print("线程运行中 {}".format(threading.current_thread().getName()))
time.sleep(1)
lk.release() # 释放锁
samapahore = threading.BoundedSemaphore(5)
if __name__ == '__main__':
for i in range(10): # 一次执行5个, 其它的等线程完成在继续执行
t = threading.Thread(target=run, args=(i, samapahore))
t.start()
# 结果
# 运行后,可以看到5个一批的线程被放行, 使用信号量 不建议用来操作数据
3.4、线程队列-queue
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
import queue
import time
import threading
# 创建队列实例, 用于存储任务
qu = queue.Queue()
def do_job():
while True:
i = qu.get()
time.sleep(1)
print("index: {0} 当前: {1}".format(i, threading.current_thread()))
qu.task_done()
if __name__ == '__main__':
for i in range(3):
t = threading.Thread(target=do_job)
t.setDaemon(True)
t.start()
time.sleep(3)
# 模拟创建线程池3秒后塞进10个任务到队列
for i in range(10):
qu.put(i)
qu.join()
执行流程:
# 先执行主线程, for 循环三次get无任何数据,到 主线程的第二个for, put数据到队列中, 然后在执行do_job三次, 取出并 print当前数据以及当前的线程名称, 直到主线程执行完,程序即结束
index: 0 当前: <Thread(Thread-1, started daemon 2644)>
index: 1 当前: <Thread(Thread-2, started daemon 6240)>
index: 2 当前: <Thread(Thread-3, started daemon 2348)>
index: 3 当前: <Thread(Thread-2, started daemon 6240)>
.....
```
**具体工作描述**
1. 创建Queue.Queue()实例,然后对它填充数据或任务
2. 生成守护线程池,把线程设置成了daemon守护线程
3. 每个线程无限循环阻塞读取queue队列的项目item,并处理
4. 每次完成一次工作后,使用queue.task_done()函数向任务已经完成的队列发送一个信号
5. 主线程设置queue.join()阻塞,直到任务队列已经清空了,解除阻塞,向下执行
定时器Timer
定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。一个简单但很实用的东西。
from threading import Timer
def hello():
print("hello")
t = Timer(1, hello)
t.start()
具体工作描述
- 创建Queue.Queue()实例,然后对它填充数据或任务
- 生成守护线程池,把线程设置成了daemon守护线程
- 每个线程无限循环阻塞读取queue队列的项目item,并处理
- 每次完成一次工作后,使用queue.task_done()函数向任务已经完成的队列发送一个信号
- 主线程设置queue.join()阻塞,直到任务队列已经清空了,解除阻塞,向下执行
定时器Timer
定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。一个简单但很实用的东西。
from threading import Timer
def hello():
print("hello")
t = Timer(1, hello)
t.start()