Python基础-进程和线程
多任务介绍
1.现实中多任务
吃着饭看电影
2.计算机中的多任务
计算机中多任务处理是指:操作系统同一时段内进行多项任务的处理.
多任务是指用户在同一时间段内运行多个应用程秀,每个运用程序都可以被称为一个任务
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
并发与并行
- 并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
- 并行处理(Parallel Processing):是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集
[外链图片转存失败(img-Oaw8dKf2-1565178293865)(C:\Users\Lenovo\Desktop\Python\图片\并行和并发.png)]
进程
一个程序的执行实例被称之为进程,每个进程都能提供程序执行所需的所有资源,因此进程本质上就是资源的集合.
进程包括:
- 虚拟地址空间
- 可执行代码
- 操作系统接口
- 安全的上下文(记录启动该进程的用户,权限等等)
- 唯一的进程ID,环境变量
- 优先级类
- 最小和最大的工作空间(内存空间)
- 至少一个线程
进程的创建:fork()
python的os模块封装了常见的系统调用函数,其中就包括了fork(),可以让我们在程序中轻松创建子进程
import os
pid=os.fork()
if pid==0:
print('zhengjiang University.')
else:
print('city college.')
在Unix/Linux中,提供了fork()系统函数
普通的函数调用时调用一次返回一次,而fork调用一次返回两次,因为操作系统会自动把当前的进程(父进程)复制一份(子进程),然后分别在父进程和子进程返回.fork()子进程永远返回0,而父进程返回子进程的ID
一个父进程可以fork出很多子进程,父进程可以记下每个子进程的ID二子进程只需调用getppid()
- getpid()返回当前进程标识
- getppid()返回父进程标识
demo:
import os
pid=os.fork()
if pid < 0:
print('fork 调用失败')
elif pid==0:
print('我是子进程:\t%s,我的父进程是:\t %s'%(os.getpid(),os.getppid()))
else:
print('我是fu进程:\t%s,我的子进程是:\t %s'%(os.getpid(),pid))
print('父子进程都可以执行这句')
'''
我是fu进程: 2639,我的子进程是: 2640
父子进程都可以执行这句
我是子进程: 2640,我的父进程是: 2639
父子进程都可以执行这句
'''
线程
线程是操作系统能够运算调度的最小单位.线程被包含进程之中,是进程中的实际运作单位.一条线程是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并发执行不同的任务.
一条线程是一个execution context(执行上下文),即一个和cpu执行时所需要的一串指令.
线程的工作方式
假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。
线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。
进程与线程的区别
- 同一进程中的线程共享同一内存空间,单相互之间是独立的.
- 同一进程中的所有数据是共享的,数据之间是独立的
- 对主线程的修改可能会影响其他线程的行为,但是父进程的修改不会影响其他子进程
- 线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
- 同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
- 创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
- 一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
- 线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
但是要注意的是:进程与线程一样有着五个阶段:创建、就绪、运行、阻塞、终止。
Python中多线程的实现
import threading
import time
def download_music():
for i in range(5):
time.sleep(1)
print('--正在下载歌曲%d--'%i)
def play_music():
for i in range(5):
time.sleep(1)
print('----正在播放歌曲%d----'%i)
def main():
#创建两个线程对象
t1=threading.Thread(target=download_music)
t2=threading.Thread(target=play_music)
t1.start()
t2.start()
if __name__=='__main__':
main()
- 可以看出使用多线程时可以节省时间
- 当我们使用start()时,才会执行线程中的代码
继承Thread类,创建一个新的class,将要执行的代码写道run函数里
demo:
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(5):
time.sleep(1)
#name 保存的时当前线程的名字
msg='I am '+self.name+'@'+str(i)
print(msg)
if __name__=='__main__':
t1=MyThread()
t1.start()
Python的threading.Thread类中有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法.而创建自己的线程实例后,通过Thread的start方法,可以启动该线程.当该线程获得执行的机会时,就会调用run方法执行线程
注:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
线程的开始与结束
- 子线程何时开始,何时运行
- 当调用thread.start(),开启线程,再运行线程的代码
- 子线程何时结束
- 子线程把target指向函数中的语句执行完毕后,或者线程中的run代码执行完毕后,立即结束当前子进程
- 查看当前的线程数量
- 通过threading.enumrate()可枚举当前运行的所有线程
- 主线程何时结束
- 所有子线程执行完毕后,主线程结束
demo:
import threading
import time
def test1():
for i in range(5):
time.sleep(1)
print('----子线程1-----%d'%i)
print('子线程中查看线程情况:',threading.enumerate())
def test2():
for i in range(10):
time.sleep(1)
print('----子线程2-----%d'%i)
print('子线程中查看线程情况:',threading.enumerate())
def main():
#thread
print('创建线程之前的线程情况',threading.enumerate())
t1=threading.Thread(target=test1)
t2=threading.Thread(target=test2)
t1.start()
t2.start()
time.sleep(1)
print('调用thread。start()之后的线程情况',threading.enumerate())
t2.join()
print('查看当前线程',threading.enumerate())
if __name__=='__main__':
main()
线程的执行顺序
按照正常的逻辑,是没有办法去对线程的执行顺序进行控制的,因为线程的执行完全是自发地去抢CPU时间片,谁抢到谁先执行。
但是可以通过使用互斥锁来实现,举例在同步一节。
多线程共享全局变量问题
多线程可能会遇到的问题
假设有两个线程t1和t2,都要对一个变量g_num进行运算+1,两个线程t1和t2分别对g_num各加十次,g_num的最终结果应该是10 但是我们运行程序会发现结果为20.
import threading
import time
g_num=0
def work1(num):
global g_num
for i in range(num):
g_num+=1
print('-----in work 1,g_num is %d'%g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print('-----in work 2,g_num is %d' % g_num)
print('----线程创建之前g_num: %d'%g_num)
t1=threading.Thread(target=work1,args=(10,))
t2=threading.Thread(target=work2,args=(10,))
t1.start()
t2.start()
while len(threading.enumerate()) !=1:
time.sleep(1)
print('两个线程对同一个变量的操作结果:%d'%g_num)
'''
----线程创建之前g_num: 0
-----in work 1,g_num is 10
-----in work 2,g_num is 20
两个线程对同一个变量的操作结果:20
'''
一种可能的情况:
在num=0时,t1取得num=0,此时系统把t1调度’sleeping’状态,t2转换为’running’状态,t2也获得了num=0.然后,t2进行加一并赋给num。接着,系统将t2调度为’sleeping’,t1为’running’,t1完成之前的得到的num=0加1后赋给num。这种情况,两个进程都完成一次加一但是num还是等于1.但是最后num=20,那么线程肯定不是这样运行的。
如果我们将两个程序的参数调整为1 000 000,多次运行,结果不同。
说明,如果多线程同时对一个全局变量进行操作,会出现资源竞争问题,从而使数据结果不正确,导致线程安全问题
同步
同步就是协同步调,按照预定的先后次序进行运行。好比交流信息,一个人说完,另一个人再说。
进程和线程的同步,可以理解为进程A和线程B相互配合,A执行到一定程度需要B的结果继续用作运行,于是停止,B开始运行直至得到相应结果并将结果赋给A,A再次开始运行,循环往复,直至程序结束。
计算错误结果的解决
通过‘线程同步’进行解决
思路:
- 系统调度t1,获取num=0,此时上一把锁,不允许其他线程此时操作num
- 对num的值加1
- 解锁,此时num的值为1,其他的线程就可以使用num=1
- 同理,其他线程对num进行修改时也会使用先前的方法
这样就可以保证数据的正确性。
互斥锁
当多个线程几乎同时修改某个贡献数据时,需要进行同步控制
线程同步保证多个线程安全访问竞争资源,最简单的同步机制就是引入互斥锁。
互斥锁就是为我们的资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源被锁定,其他线程不能对其进行修改,直到该线程释放资源,解除锁定
互斥锁保证了每次只有一个线程写入操作,从而保证了多线程情况下数据的正确性。
在threading模块内部,定义了Lock()类,可以方便进行资源锁定
mutex=threading.Lock()#创建锁
mutex.acquire([blocking])#锁定
mutex.release()#释放
说明:
- blocking
- True,则当前线程堵塞,直到获取到这个锁为止(若无指定,则默认为True)
死锁
在线程间共享多个资源时,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁。
例如:
一个线程T1获得了对资源R1的访问权。
一个线程T2获得了对资源R2的访问权。
T1请求对R2的访问权但是由于此权力被T2所占而不得不等待。
T2请求对R1的访问权但是由于此权力被T1所占而不得不等待。
T1和T2将永远维持等待状态,此时我们陷入了死锁的处境!这种问题比你所遇到的大多数的bug都要隐秘,针对此问题主要有三种解决方案:
- 在同一时刻不允许一个线程访问多个资源。
- 为资源访问权的获取定义一个关系顺序。换句话说,当一个线程已经获得了R1的访问权后,将无法获得R2的访问权。当然,访问权的释放必须遵循相反的顺序。
- 为所有访问资源的请求系统地定义一个最大等待时间(超时时间),并妥善处理请求失败的情况。几乎所有的.NET的同步机制都提供了这个功能。
前两种技术效率更高但是也更加难于实现。事实上,它们都需要很强的约束,而这点随着应用程序的演变将越来越难以维护。尽管如此,使用这些技术不会存在失败的情况。
死锁很少发生,但一旦发生就会造成应用停止响应。
import threading
import time
printer_mutex = threading.Lock() # 打印机锁
paper_mutext = threading.Lock() # 纸张锁
class ResumeThread(threading.Thread):
"""编写个人简历任务的线程"""
def run(self):
print("ResumeThread:编写个人简历任务")
# 使用打印机资源,先对打印机加锁
printer_mutex.acquire()
print("--ResumeThread:正在使用打印机资源--")
time.sleep(1) # 休眠1秒
# 使用纸张耗材,先对纸张耗材加锁
paper_mutext.acquire()
print("--正在使用纸张资源--")
time.sleep(1)
paper_mutext.release() # 释放纸张锁
# 释放打印机锁
printer_mutex.release()
class PaperListThread(threading.Thread):
"""盘点纸张耗材任务的线程"""
def run(self):
print("PaperListThread:盘点纸张耗材任务")
# 使用纸张耗材,先对纸张耗材加锁
paper_mutext.acquire()
print("--PaperListThread:正在盘点纸张耗材--")
time.sleep(1) # 休眠1秒
# 使用打印机资源,打印清单
printer_mutex.acquire()
print("--正在使用打印机资源--")
time.sleep(1)
printer_mutex.release() # 释放打印机锁
# 释放纸张耗材锁
paper_mutext.release()
if __name__ == '__main__':
t1 = ResumeThread()
t2 = PaperListThread()
t1.start()
t2.start()
同步的应用
让多个程序有序执行
import threading
import time
class Task1(threading.Thread):
def run(self):
while True:
if lock1.acquire():
print('-----Task1------')
time.sleep(0.5)
lock2.release()
class Task2(threading.Thread):
def run(self):
while True:
if lock2.acquire():
print('-----Task2------')
time.sleep(0.5)
lock3.release()
class Task3(threading.Thread):
def run(self):
while True:
if lock3.acquire():
print('-----Task3------')
time.sleep(0.5)
lock1.release()
lock1=threading.Lock()
#创建锁2,并且锁上
lock2=threading.Lock()
lock2.acquire()
#创建锁3,并且锁上
lock3=threading.Lock()
lock3.acquire()
t1=Task1()
t2=Task2()
t3=Task3()
t1.start()
t2.start()
t3.start()
生产消费问题—(竞态条件)
也就优先缓冲问题,是一个多线程同步的经典案例
描述了一个两个固定大小缓冲区的线程——即所谓’生产者’和’消费者’——在实际运行时会发生的问题
生产者的主要作用,生产一定量的数据放在缓冲区,然后重复此过程。
与此同时,消费者也在缓冲区消耗这些数据。
整个问题的关键是,生产者不会在缓冲区满时加入数据,消费者也不会再缓冲区空时,消费数据。
解决办法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形
1.队列:先进先出
2.栈:先进后出
Python中queue(Py3,在Py2中是Queue)模块提供了一个同步的,线程安全的队列类,包括先入先出(FIFO)队列Queue和后入先出(LIFO)队列LifeQueue以及优先级队列PriorityQueue。
这些队列实现了锁原语(原子操作,要么不做,要么做完),可以在线程中直接使用
可以使用队列来实现线程间的同步
FIFO队列实现生产者消费者问题
import threading
import time
from queue import Queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count = count +1
msg = '生成产品'+str(count)
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
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))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()
sumer(threading.Thread):
def run(self):
global queue
while True:
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))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()