Python菜鸟编程第十六课之线程和进程
1.多任务介绍
计算机中的多任务是指:操作系统同时完成多项任务的处理。
多任务处理是指:用户再同一时间段内运行多个应用程序,每个应用程序就可以称之为一个任务。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
1.1并发与并行
- 并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行
- 并行处理(Parallel Processing):是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集
demo:
import time
def download_music():
"""模拟下载歌曲,需要5秒钟下载完成"""
for i in range(5):
time.sleep(1) # 休眠1秒
print("-----正在下载歌曲%d----" % i)
def play_music():
"""模拟播放歌曲,需要5秒钟播放完成"""
for i in range(5):
time.sleep(1) # 休眠1秒
print("-----正在播放歌曲%d----" % i)
def main():
# 先下载歌曲
download_music()
# 再播放歌曲
play_music()
if __name__ == '__main__':#单独运行,后面的语句可以执行,当做模块运行时,后面语句不会运行。
main()
2.进程
进程:一个程序的执行实例。每个进程提供执行程序所需的所有资源。本质上是资源的合集。
一个进程有虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录该进程的用户和权限)、唯一的进程ID、环境变量、优先级类、最大和最小的工作空间(内存空间),还要至少有一个线程。
进程的创建:fork()
Python的os模块封装了常见的系统调用函数,其中包含fork(),可以让我们网程序中轻松的创建子进程
demo:
import os
pid =os.fork()
if pid == 0:
print("zhejiang university")
else:
print("citycollege")
运行结果:
citycollege
zhejiang university
在Unix/Linux,提供了fork()系统函数。
- 普通的函数调用,调用一次,返回一次
- fork()调用一次,返回两次。因为操作系统自动把当前的进程(父进程)复制一份(子进程),然后分别在父进程和子进程内返回。
fork()子进程永远返回0,而父进程返回子进程的ID
一个父进程可以fork出很多子进程。父进程可以记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
- 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("我是子进程: \t %s,我打父进程是: \t %s" % (os.getpid(),pid))
print("父子进程都可以执行这里")
运行结果:
我是子进程: 3508,我打父进程是: 3509
父子进程都可以执行这里
我是子进程: 3509,我打父进程是: 3508
父子进程都可以执行这里
3.线程
线程是操作系统能够运算调度的最小单位。线程被包含在进程之中,是进程中的实际运作单位。一条线程是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并行执行不同的任务。一条线程是一个execution context(执行上下文),即一个CPU执行是所需要的一串指令。
3.1进程的工作方式
假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。
线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。
3.2进程与线程的区别
同一进程中的线程共享同一内存空间,但是进程之间是独立的。
同一个进程中的所有线程的数据共享,进程之间的数据是独立的。
对主线程的修改可能影响其他线程的行为,但是父进程的修改(除了删除以为)不会影响其他子进程
线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
demo:
import threading
import time
def download_music():
for i in range(5):
time.sleep(1)
print('----正在下载歌曲%s----'%i)
def play_music():
for i in range(5):
time.sleep(1)
print('----正在播放歌曲%s----' % i)
def main():
#创建两个线程对象,target指向新开启的线程要执行的函数
t1=threading.Thread(target=download_music)
t2 = threading.Thread(target=play_music)
t1.start()
t2.start()
if __name__=='__main__':
main()
运行结果:
----正在下载歌曲0--------正在播放歌曲0----
----正在下载歌曲1--------正在播放歌曲1----
----正在下载歌曲2--------正在播放歌曲2----
----正在下载歌曲3----
----正在播放歌曲3----
----正在下载歌曲4----
----正在播放歌曲4----
1.可以明显看到使用多线程并发的操作,花费时间更多
2.当我们调用start()时,才会真正执行线程以及线程中的代码。
demo2:
import threading
import time
class MyThreading(threading.Thread):
def run(self):
for i in range(5):
time.sleep(1)
# self.name 保存的是当前线程的名字
msg = 'I am ' + self.name + '@' + str(i)
print(msg)
if __name__ == '__main__':
t1 = MyThreading()
t2 = MyThreading()
t1.start()
t2.start()
运行结果:
I am Thread-1@0
I am Thread-2@0
I am Thread-1@1
I am Thread-2@1
I am Thread-1@2
I am Thread-2@2
I am Thread-1@3
I am Thread-2@3
I am Thread-1@4
I am Thread-2@4
Python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。创建自己的线程实例后,通过Thread的start()方法,可以启动该线程。当该线程获得执行的机会时,就会调用run()方法执行线程。
函数名 | 描述 |
---|---|
threading.currentThread() | 返回当前的线程变量。 |
threading.enumerate() | 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 |
threading.activeCount() | 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。 |
3.3线程何时开启,何时结束?
3.31子线程
当调用thread.start()时,开启线程,再运行线程的代码。
子线程吧target指向的函数中的语句执行完毕后,或者线程中run代码执行完毕后,立即结束当前子进程。
查看当前的线程数量:通过threading.enumerate()可枚举当前运行的所有线程
3.32主线程
当所有主线程执行完毕后,主线程才结束
demo:
import threading
import time
def test1():
for i in range(2):
time.sleep(3)
print('----子线程1----%d' % i)
print('子线程1中查看线程情况', threading.enumerate())
def test2():
for i in range(4):
time.sleep(3)
print('----子线程2----%d' % i)
print('子线程2中查看线程情况', threading.enumerate())
def main():
# threading.enumerate()可枚举当前运行的所有线程
print('创建线程之前的线程情况:', threading.enumerate())
# 创建线程对象
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
time.sleep(3)
print('创建线程之后的线程情况:', threading.enumerate())
t1.start()
t2.start()
time.sleep(20)
print('调用了thread.start()之后的线程情况', threading.enumerate())
t2.join() # 当t2线程执行完毕后,在执行后续的代码
print('查看当前线程', threading.enumerate())
if __name__ == '__main__':
main()
运行结果:
创建线程之前的线程情况: [<_MainThread(MainThread, started 1636)>]
创建线程之后的线程情况: [<_MainThread(MainThread, started 1636)>]
----子线程1----0
子线程1中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-1, started 25396)>, <Thread(Thread-2, started 11952)>]
----子线程2----0
子线程2中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-1, started 25396)>, <Thread(Thread-2, started 11952)>]
----子线程1----1
子线程1中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-1, started 25396)>, <Thread(Thread-2, started 11952)>]
----子线程2----1
子线程2中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-2, started 11952)>]
----子线程2----2
子线程2中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-2, started 11952)>]
----子线程2----3
子线程2中查看线程情况 [<_MainThread(MainThread, started 1636)>, <Thread(Thread-2, started 11952)>]
调用了thread.start()之后的线程情况 [<_MainThread(MainThread, started 1636)>]
查看当前线程 [<_MainThread(MainThread, started 1636)>]
运行上述代码的朋友,应该会有人发现,其实每次运行的结果都是不一样的。想要每次结果都一样,两种方法可以供大家使用。
1.上锁
import threading
import time
lock = threading.Lock()
def test1():
lock.acquire()
for i in range(2):
time.sleep(3)
print('----子线程1----%d' % i)
print('子线程1中查看线程情况', threading.enumerate())
lock.release()
def test2():
lock.acquire()
for i in range(4):
time.sleep(3)
print('----子线程2----%d' % i)
print('子线程2中查看线程情况', threading.enumerate())
lock.release()
def main():
# threading.enumerate()可枚举当前运行的所有线程
print('创建线程之前的线程情况:', threading.enumerate())
# 创建线程对象
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
time.sleep(3)
print('创建线程之后的线程情况:', threading.enumerate())
t1.start()
t2.start()
time.sleep(2)
print('调用了thread.start()之后的线程情况', threading.enumerate())
t2.join() # 当t2线程执行完毕后,再执行后续的代码
print('查看当前线程', threading.enumerate())
if __name__ == '__main__':
main()
2.使用join()方法
import threading
import time
# lock = threading.Lock()
def test1():
# lock.acquire()
for i in range(2):
time.sleep(3)
print('----子线程1----%d' % i)
print('子线程1中查看线程情况', threading.enumerate())
# lock.release()
def test2():
# lock.acquire()
for i in range(4):
time.sleep(3)
print('----子线程2----%d' % i)
print('子线程2中查看线程情况', threading.enumerate())
# lock.release()
def main():
# threading.enumerate()可枚举当前运行的所有线程
print('创建线程之前的线程情况:', threading.enumerate())
# 创建线程对象
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test2)
time.sleep(3)
print('创建线程之后的线程情况:', threading.enumerate())
t1.start()
t1.join()
t2.start()
t2.join()# 当t2线程执行完毕后,再执行后续的代码
time.sleep(2)
print('调用了thread.start()之后的线程情况', threading.enumerate())
print('查看当前线程', threading.enumerate())
if __name__ == '__main__':
main()
两个方法最后的运行结果都一致:
运行结果:
创建线程之前的线程情况: [<_MainThread(MainThread, started 16828)>]
创建线程之后的线程情况: [<_MainThread(MainThread, started 16828)>]
----子线程1----0
子线程1中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-1, started 19496)>]
----子线程1----1
子线程1中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-1, started 19496)>]
----子线程2----0
子线程2中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-2, started 9548)>]
----子线程2----1
子线程2中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-2, started 9548)>]
----子线程2----2
子线程2中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-2, started 9548)>]
----子线程2----3
子线程2中查看线程情况 [<_MainThread(MainThread, started 16828)>, <Thread(Thread-2, started 9548)>]
调用了thread.start()之后的线程情况 [<_MainThread(MainThread, started 16828)>]
查看当前线程 [<_MainThread(MainThread, started 16828)>]
需要注意的是:使用互斥锁时,如果只上一把锁,等于没上,即默认没上锁。