Python基础之多线程概念
1、程序,进程,线程,多进程,多线程
首先,先来了解一下多线程中的几个概念:
在学校老师讲操作系统时,经常说的一句话就是:
进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 怎么理解呢?
简单地来说,线程是进程中的一个执行任务(控制单元),负责进程中程序的执行。一个进程至少有一个线程,一个进程中的多个线程可以共享数据。所以线程又被成为轻量级进程。 - 多进程:
对于开发来说,多进程的概念更趋向于多个进程同时去完成一项工作。比如在Python开发中,Python在实现Python解释器(CPython)时引入GIL锁,使得一个进程任何时候仅有一个线程能执行
,这样一来,多线程的效率可能还比不上单线程。因此使用多进程来避免这个限制。 - 多线程:
再来说说多线程,当你在使用一个文本程序时,该程序由三个进程组成:输入接收进程A,显示进程B,写入硬盘进程C。而他们共同需要的东西就是文本,因此三者就难免需要通信,而进程间频繁的通信势必会损失性能,因此,如果在一个进程中使用多线程来完成这个程序,一个进程内的多线程通信效率高,这样就能够保证性能了。 - 总的来说,这个结构是这样的:
程序 -> 进程 -> 线程
2、线程的生命周期
- 1、New(新建):新创建的线程经过初始化,进入Runnable(就绪)状态。
- 2、Runnbale(就绪):等待线程调度,调度后进入Running(运行)状态。
- 3、Running(运行):线程正常运行,期间可能会因为某些情况进入Blocked(堵塞)状态。(同步锁:调用和sleep()和join()方法进入Sleeping状态;执行wait()方法进入Waiting状态,等待其他线程notify通知唤醒)。
- 4、Blocked(阻塞):线程暂停运行,解除阻塞后进入Runnable(就绪)状态重新等待调度。
- 5、Dead(死亡):线程完成了它的任务正常结束或因异常导致终止。
3、并行与并发
- 并行:
指两个或多个任务同时执行,即同时做不同事的能力。例如垃圾回收时,多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 - 并发:
指两个或多个任务在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续执行,而垃圾收集程序执行于另一个CPU上。
并行是同时处理多个任务,而并发则是处理多个任务,但不一定要同时,并行可以说是并发的子集。
4、同步与异步
- 同步:线程执行某个请求,如果该请求需要一段时间才能返回信息,那么这个线程会一直等待,直到收到返回信息才能继续执行下去。
- 异步:线程执行完某个请求,不需要一直等,直接执行后续操作,当有消息返回时系统会通知线程进程处理,这样可以提高执行的效率;异步在网络请求的应用非常常见。
5、如何解决同步安全问题(同步锁)
当多个线程访问临界资源(共享资源)的时候,有可能会出现线程安全问题;基本所有并发模式在解决线程安全问题时都采用“系列化访问资源”的方式,就是同一时刻,只有一个线程访问临界资源,也称“同步互斥访问”。通常的操作就是加锁(同步锁)。当有线程访问临界资源时需要获得这个锁,其他线程无法访问,只能等待(堵塞),等这个线程使用完释放锁,供其他线程继续访问。
6、与锁有关的特殊情况:死锁,饥饿与活锁
单单有了同步锁不意味着就一了百了了,当多个进程或线程的操作设计到了多个锁,就可能出现如下三种情况:
死锁
两个或以上的线程在执行中,因争夺资源而产生的一种互相等待的现象。若无外力作用,它们将无法推进下去。
死锁发生的条件:
- 1、互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
- 2、请求和保持条件:线程T1至少已经爆出了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是线程T1也必须等待,但又对自己保持的资源R1不释放。
- 3、不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
- 4、环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即进程P0等待P1占用的资源,P1等待P2占用的资源,Pn等待P0占用的资源。
饥饿与饿死
是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求。。。。。。所以T2可能永远等待。
活锁
是指线程1可以使用资源,但它让其他线程先使用资源;线程2可以使用资源,但它也让其他线程先使用资源,于是两者一直谦让,都无法使用资源。
因此避免活锁的方法时采用先来先服务的策略。当多个事物请求封锁同一数据对象时,封锁子系统按请求封锁的先后顺序对事务进行排队。
7、守护线程
也叫后台线程,是一种为其他线程提供服务的线程。比如一个简单的例子:
当两个线程在协同做一件事,如果有一个线程死掉,事情就无法继续下去,此时就可以引入守护线程,轮询地去判断两个线程是否死掉,如果死掉就start开启线程,在Python中可以在线程初始化的时候调用setDaemon(True)把线程设置为守护线程,如果程序中只剩下守护线程,则自动退出。
- 守护线程的作用:
守护线程作用是为其他线程提供便利服务,守护线程最经典的应用就是GC(垃圾收集器) - 守护线程的特点:
只要当前主线程中尚存任何一个非守护线程没有结束,守护线程就全部工作。
只有当最后一个非守护线程结束时,守护线程随着主线程一同结束工作。
代码示例:
1、线程为非守护线程
import time
import threading
def fun():
print("start fun")
time.sleep(2)
print("end fun")
def main():
print("main thread")
t1 = threading.Thread(target=fun,args=())
t1.setDaemon(False)
t1.start()
time.sleep(1)
print("main thread end")
if __name__ == '__main__':
main()
执行结果如下:
main thread
start fun
main thread end
end fun
由此可见,程序在等待子线程结束,才会退出。
import time
import threading
def fun():
print("start fun")
time.sleep(2)
print("end fun")
def main():
print("main thread")
t1 = threading.Thread(target=fun,args=())
t1.setDaemon(True)
t1.start()
time.sleep(1)
print("main thread end")
if __name__ == '__main__':
main()
结果如下:
main thread
start fun
main thread end
说明:程序在主线程结束后,直接退出了。导致子线程没有运行完。
线程并发的经典问题:生产者与消费者
在线程中,生产者就是生产数据的线程,消费之就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者了。为了解决这个问题于是引入了生产者和消费者模式。
1、生产者消费者模型
生产者:比喻的是程序中负责产生数据的任务
消费者:比喻的是程序中负责处理数据的任务
生产者 --》共享的介质(队列)《–消费者
2、使用的优点
当我们的进程中存在明显的两类任务,一类负责产生数据,另一类负责处理数据
当时就应该考虑使用生产者消费者模型来提升程序的效率。
3、例子说明
下面我们实现这样一个过程
- 维护一个整数列表interger_list,共有两个线程
- Producer类对应一个线程,功能:随机产生一个整数,加入整数列表中
- Consumer类对应一个线程,功能:从整数列表中pop掉一个整数
- 通过time.sleep来表示两各线程运行速度,设置成Producer 产生的速度没有Consumer消耗的快
import time
import threading
import random
class Producer(threading.Thread):
def __init__(self, condition, integer_list):
threading.Thread.__init__(self)
self.condition = condition
self.integer_list = integer_list
def run(self):
while True:
random_integer = random.randint(0, 100)
with self.condition:
self.integer_list.append(random_integer)
print('integer list add integer {}'.format(random_integer))
self.condition.notify()
time.sleep(1.2 * random.random())
class Consumer(threading.Thread):
def __init__(self, condition, integer_list):
threading.Thread.__init__(self)
self.condition = condition
self.integer_list = integer_list
def run(self):
while True:
with self.condition:
if self.integer_list:
integer = self.integer_list.pop()
print('integer list lose integer {}'.format(integer))
time.sleep(random.random())
else:
print('there is no integer in the list')
self.condition.wait()
def main():
integer_list = []
condition = threading.Condition()
th1 = Producer(condition, integer_list)
th2 = Consumer(condition, integer_list)
th1.start()
th2.start()
if __name__ == '__main__':
main()
在生产出整数时,notify通知wait的线程可依据虚了
消费者查询到列表为空时调用wait等待通知(notify)