进程和线程
现代的操作系统基本上都是支持多任务
的操作系统
那?什么叫多任务
呢,简单而言,就是操作系统可以同时运行多个任务。
就像是上图这种,可以同时运行多个任务,当然还有很多的后台应用也在默默的运行着,只是没有显示在桌面而已
这里就要谈到单核和多核CPU
了
单核CPU在执行任务时,通常都是任务1执行一个单位时间,任务2执行一个单位时间,,再切换到任务3,任务4.... 这样反复切换。表面而言,这些任务切换时都是交替执行的,但是,由于CPU
执行速度实在太快,肉眼感知就像所有的任务都是在同时执行一样。
多核CPU真正的并行执行多任务只能在多核CPU
上实现,但是,由于任务数量远远多于CPU
的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说,一个任务就是一个进程(Process)
。每个进程
至少要干一件事,所以,一个进程
至少有一个线程
,复杂的进程
可以有多个线程
,多个线程
可以同时执行,多线程
的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程
都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程
需要多核CPU
才可能实现。
如果我们要同时执行多个任务怎么办?
有两种解决方案:
第一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
第二种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
总结一下就是,多任务的实现有3种方式:
- 多进程模式;
- 多线程模式;
- 多进程+多线程模式。`
小结
-
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。
-
多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。
进程
先说两句:什么是进程?有什么用?——ok,那我问你,你能一手画圆一手画方吗?——我猜不能。但计算机就不一样了,一边绘制正方体一边绘制球体都是小case(屏幕上自动绘制图形),这是因为计算机启动了另一个"大脑"来处理另一个任务,即两个“大脑”分别同时画两个图形 效率X2!我们之前的写程序都是计算机一个“大脑”在工作!ok,那怎么启动计算机其他的大脑呢?——启动另一个进程就可以了!
在Unix
/Linux
操作系统中,提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的
ID
由是,一个父进程可以fork
出很多子进程,所以,父进程要记下每个子进程的ID
,而子进程只需要调用getppid()
就可以拿到父进程的ID
。
python的os
模块中就封装了常见的系统调用,其中就包括fork
在Windows
中没有fork
调用,所以无法采用fork
形式,因为python
是跨平台的,所以,就有了multiprocessing
模块,此模块就是支持跨平台版本的多进程模块
multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
执行结果如下:
Parent process 928.
Child process will start.
Run child process test (929)...
Process end.
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()
方法启动,这样创建进程比fork()
还要简单。
join()
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
简化如上操作见下
1、创建进程:
import multiprocessing
import time
def action(a, b): # 待会两个进程要执行的任务↓
for i in range(30): # 循环30次
print(a, ' ', b)
time.sleep(0.1) # 等待0.1s
if __name__ == '__main__': # 这行代码很重要,新建进程的时候都加上它!!原因不用管(我也不知道233)
jc1 = multiprocessing.Process(target=action, args=('进程一', 0)) # 准备建立一个进程:multiprocessing.Process()
jc2 = multiprocessing.Process(target=action, args=('进程二', 1)) # 再准备建立一个新进程,这是基本格式记住←
# 必要参数target:指定进程要执行的任务(这里是执行函数 action),必要参数args:直译成中文就是'参数',顾名思义就是前面target的参数,即action的参数,注意args是个元组,所以args后的参数写成tuple元组格式。直接写target('进程一',0)一定报错的
jc1.start() # 将蓄势待发的jc1进程正式启动!!
jc2.start() # 同上...
jc1.join() # 等待进程jc1将任务执行完...
jc2.join() # ...
print('jc1,jc2任务都已执行完毕')
jc1.close() # 彻底关闭进程jc1
jc2.close() # ...
#输出结果是两个进程同时且连续打印0、1
2、Pool:进程池,可以启动大量的子进程,或者批量创建子进程
import time
import os
def action1(a, b=50):
for i in range(b):
print(a, os.getpid(), ' ', i) # os.getpid(): pid简单来说就是每个进程的“身份证”
time.sleep(0.1)
if __name__ == '__main__': # 还要添加这行,否则可能出现异常
ci = Pool(3) # 创建一个进程池,容量为3个进程
ci.apply_async(action1, args=('进程一',)) # 启动第一个子进程...
ci.apply_async(action1, args=('进程二', 50)) # 和普通进程的启动方式有很大不同仔细看
ci.apply_async(action1, args=('进程三', 60)) # Pool的最基本格式记住←
# 注意:程序现在有4个进程在运行:上面的三个子进程 和一个最为核心的:主进程
ci.close() # 关闭进程池(但池子内已启动的子进程还会继续进行)
ci.join() # 等待进程池内的所有子进程完毕
print('比如说这最后的一行输出就是主进程执行任务打印出来的')
#主进程(父进程)全程干了什么?创建进程池、启动子进程、关闭进程池、等待子进程完毕、打印最后一行
代码解读:
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了
3、进程间的通信:
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据
import multiprocessing
def foo(aa):
ss = aa.get() # 管子的另一端放在子进程这里,子进程接收到了数据
print('子进程已收到数据...')
print(ss) # 子进程打印出了数据内容...
if __name__ == '__main__': # 要加这行...
tx = multiprocessing.Queue() # 创建进程通信的Queue,你可以理解为我拿了个管子来...
jc = multiprocessing.Process(target=foo, args=(tx,)) # 创建子进程
jc.start() # 启子子进程
print('主进程准备发送数据...')
tx.put('有内鬼,终止交易!') # 将管子的一端放在主进程这里,主进程往管子里丢入数据↑
jc.join()
#这种方法可以实现任意进程间的通信,这里写的是主、子进程间的通信#
Windows上的进程见下图
小结
-
在Unix/Linux下,可以使用fork()调用实现多进程。
-
要实现跨平台的多进程,可以使用multiprocessing模块。
-
进程间通信是通过Queue、Pipes等实现的。
线程
多任务可以由多进程完成,也可以由一个进程内的多线程完成
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持
在python
中,提供了两个模块:
_thread
和threading
,_thread
是低级模块,threading
是高级模块,对_thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行
import time, threading
# 新线程执行的代码:
def loop():
print('thread %s is running...' % threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name, n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
threading
模块有个current_thread()
函数,它永远返回当前线程的实例
主线程实例的名字叫MainThread
,子线程名字如果未命名,则会显示成Thread-1
,Thread-2
……
线程锁
多线程和多进程最大的不同在于
多进程
中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响多线程
中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了
例如银行存款
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(1000000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
然而实际结果却是
8
这并没有达到预期结果0
原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:
balance = balance + n
也分两步:
- 计算balance + n,存入临时变量中;
- 将临时变量的值赋给balance。
也就是可以看成:
x = balance + n
balance = x
究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。
那么该如何确保balance计算正确呢?就要给change_it()
上一把锁,当某个线程开始执行change_it()
时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it()
,只能等待,直到锁被释放后,获得该锁以后才能改。
由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。
创建一个锁就是通过threading.Lock()来实现:
balance = 0
lock = threading.Lock()
def run_thread(n): # 上锁的方式一
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
# !/usr/bin/python3
# -*-coding:UTF-8-*-
# FileName: 完整例子
import threading
balance = 0
def change_it(n):
'''先存后取,结果应该为0'''
global balance
balance = balance + n
balance = balance - n
def run_thread(n): # 上锁的方式二
'''with Lock的作用相当于自动获取和释放锁(资源)'''
with lock:
for i in range(100000):
change_it(n) # 放心地改吧
# def run_thread(n): # 上锁的方式三
# for i in range(100000):
# with lock:
# change_it(n)
def main():
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'total balance is : {balance}')
if __name__ == '__main__':
main()
当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。
获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。
锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行
坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止
小结
- 多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
- Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
实例
import time, threading
# 创建三个线程,利用线程锁,操作同一个列表中的数据
# 线程一:给列表增加一个元素
# 线程二:给列表删除一个元素
# 线程三:修改列表元素
L1 = ["python","javascript","PHP","Java"]
glock = threading.Lock()
def add_list():
glock.acquire()
time.sleep(2)
num = input("please input a data to add:
")
L1.append(num)
glock.release()
print("子线程一:", L1)
def pop_list():
glock.acquire()
time.sleep(2)
n = int(input("
please input the number of list to pop:"))
L1.pop(n)
glock.release()
print("子线程二:", L1)
def change_list():
glock.acquire()
time.sleep(2)
print(L1)
num = int(input("
please input the number of list that you need change:
"))
strs = input("
please input your data to change:
")
L1[num] = strs
glock.release()
print("子线程三:", L1)
if __name__ == '__main__':
# for i in range(4):
# 进程一:增加一个元素
t1 = threading.Thread(target=add_list, name="线程一")
# 进程二:删除一个元素
t2 = threading.Thread(target=pop_list, name="线程二")
# 进程三:修改一个元素
t3 = threading.Thread(target=change_list, name="线程三")
print("线程 %s 正在运行中" % threading.current_thread().name)
# 开启线程
t1.start()
t2.start()
t3.start()
print('主线程执行结束')
如果有帮助您,那我非常荣幸;如果有不正,非常希望您能够指出,以便改正