python线程和进程
人在^O^旅途 人气:0什么是进程
进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。
每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
什么是线程
一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。
CPU调度和分派的基本单位线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。
由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
注:当然在单核CPU系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的只有唯一的一个线程,多个线程共享了CPU的执行时间。
线程缺点:
多线程也并不是没有坏处,站在其他进程的角度,多线程的程序对其他程序并不友好,因为它占用了更多的CPU执行时间,导致其他程序无法获得足够的CPU执行时间;另一方面,站在开发者的角度,编写和调试多线程的程序都对开发者有较高的要求,对于初学者来说更加困难。
线程与进程的区别
- 地址空间和其他资源:进程间相互独立,同一进程的各线程间共享。某线程内的想爱你城咋其他进程不可见。
- 通信:进程间通信IPC,线程间可以直接读写进程数据段来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 在多线程操作系统中,进程不是一个可执行的实体
并行与并发
并行(Parallelism)
并行:指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上(多核),同时执行。
特点
- 同一时刻发生,同时执行。
- 不存在像并发那样竞争,等待的概念。
并发(Concurrency)
指一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
特点
- 微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
- 宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。
Python中的多进程
Python中进程操作
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
法:Process([group [, target [, name [, args [, kwargs]]]]])
由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)。
注意:
- 必须使用关键字方式来指定参数;
- args指定的为传给target函数的位置参数,是一个元祖形式,必须有逗号。
参数介绍:
- group:参数未使用,默认值为None。
- target:表示调用对象,即子进程要执行的任务。
- args:表示调用的位置参数元祖。
- kwargs:表示调用对象的字典。如kwargs = {'name':Jack, 'age':18}。
- name:子进程名称。
代码展示:
import os from multiprocessing import Process def func_one(): print("第一个子进程") print("子进程(一)大儿子:%s 父进程:%s" % (os.getpid(), os.getppid())) def func_two(): print("第二个子进程") print("子进程(二)二儿子:%s 父进程:%s" % (os.getpid(), os.getppid())) if __name__ == '__main__': p_one = Process(target=func_one) P_two = Process(target=func_two) p_one.start() P_two.start() print("子进程:%s 父进程:%s" % (os.getpid(), os.getppid()))
继承Process的方式开启进程的方式:
import os from multiprocessing import Process def func_one(): print("第一个子进程") print("子进程(一)大儿子:%s 父进程:%s" % (os.getpid(), os.getppid())) def func_two(): print("第二个子进程") print("子进程(二)二儿子:%s 父进程:%s" % (os.getpid(), os.getppid())) if __name__ == '__main__': p_one = Process(target=func_one) P_two = Process(target=func_two) p_one.start() P_two.start() print("子进程:%s 父进程:%s" % (os.getpid(), os.getppid()))
线程
Python的threading模块
在Python早期的版本中就引入了thread模块(现在名为_thread)来实现多线程编程,然而该模块过于底层,而且很多功能都没有提供,因此目前的多线程开发我们推荐使用threading模块,该模块对多线程编程提供了更好的面向对象的封装。
from random import randint from threading import Thread from time import time, sleep def download(filename): print('开始下载%s...' % filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下载完成! 耗费了%d秒' % (filename, time_to_download)) def main(): start = time() t1 = Thread(target=download, args=('Python从入门到住院.pdf',)) t1.start() t2 = Thread(target=download, args=('Peking Hot.avi',)) t2.start() #join阻塞完成任务 t1.join() t2.join() end = time() print('总共耗费了%.3f秒' % (end - start)) if __name__ == '__main__': main()
我们可以直接使用threading模块的Thread
类来创建线程,但是我们之前讲过一个非常重要的概念叫“继承”,我们可以从已有的类创建新类,因此也可以通过继承Thread
类的方式来创建自定义的线程类,然后再创建线程对象并启动线程。
代码如下所示:
from random import randint from threading import Thread from time import time, sleep class DownloadTask(Thread): def __init__(self, filename): super().__init__() self._filename = filename def run(self): print('开始下载%s...' % self._filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下载完成! 耗费了%d秒' % (self._filename, time_to_download)) def main(): start = time() t1 = DownloadTask('Python从入门到住院.pdf') t1.start() t2 = DownloadTask('Peking Hot.avi') t2.start() t1.join() t2.join() end = time() print('总共耗费了%.2f秒.' % (end - start)) if __name__ == '__main__': main()
锁Lock:
模拟场景:
演示了100个线程向同一个银行账户转账(转入1元钱)的场景,在这个例子中,银行账户就是一个临界资源,在没有保护的情况下我们很有可能会得到错误的结果。
from time import sleep from threading import Thread class Account(object): #初始化账户余额为0元 def __init__(self): self._balance = 0 # 存款函数 def deposit(self, money): # 计算存款后的余额 new_balance = self._balance + money # 模拟受理存款业务需要0.01秒的时间 sleep(0.01) # 修改账户余额 self._balance = new_balance # set和get @property def balance(self): return self._balance class AddMoneyThread(Thread): def __init__(self, account, money): super().__init__() self._account = account self._money = money def run(self): self._account.deposit(self._money) def main(): # 创建对象 account = Account() threads = [] # 创建100个存款的线程向同一个账户中存钱 for _ in range(100): t = AddMoneyThread(account, 1) threads.append(t) t.start() # 等所有存款的线程都执行完毕 for t in threads: t.join() print('账户余额为: ¥%d元' % account.balance) if __name__ == '__main__': main()
运行结果:
100个线程分别向账户中转入1元钱,结果居然远远小于100元。
之所以出现这种情况是因为我们没有对银行账户这个“临界资源”加以保护,多个线程同时向账户中存钱时,会一起执行到new_balance = self._balance + money
这行代码,多个线程得到的账户余额都是初始状态下的0
,所以都是0
上面做了+1的操作,因此得到了错误的结果。
在这种情况下,“锁”就可以派上用场了。我们可以通过“锁”来保护“临界资源”,只有获得“锁”的线程才能访问“临界资源”,而其他没有得到“锁”的线程只能被阻塞起来,直到获得“锁”的线程释放了“锁”,其他线程才有机会获得“锁”,进而访问被保护的“临界资源”。
加锁:
from time import sleep from threading import Thread, Lock class Account(object): def __init__(self): self._balance = 0 self._lock = Lock() def deposit(self, money): # 先获取锁才能执行后续的代码 self._lock.acquire() try: new_balance = self._balance + money sleep(0.01) self._balance = new_balance finally: # 在finally中执行释放锁的操作保证正常异常锁都能释放 self._lock.release() @property def balance(self): return self._balance class AddMoneyThread(Thread): def __init__(self, account, money): super().__init__() self._account = account self._money = money def run(self): # 运行存钱业务,只有获取锁的才能执行 self._account.deposit(self._money) def main(): account = Account() threads = [] #创建100个线程 for _ in range(100): # 线程加钱 t = AddMoneyThread(account, 1) threads.append(t) t.start() for t in threads: t.join() print('账户余额为: ¥%d元' % account.balance) if __name__ == '__main__': main()
结果:账户余额为: ¥100元
比较遗憾的一件事情是Python的多线程并不能发挥CPU的多核特性,因为Python的解释器有一个“全局解释器锁”(GIL)的东西,任何线程执行前必须先获得GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。
全局解释器锁(GIL)
GIL是一个互斥锁,它防止多个线程同时执行Python字节码。这个锁是必要的,主要是因为CPython的内存管理不是线程安全的 尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。
因此,解释器实际上被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。在多线程环境中,Python 虚拟机按以下方式执行:
- 设置GIL
- 切换到一个线程去执行
- 运行
由于GIL的存在,Python的多线程不能称之为严格的多线程。因为多线程下每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程在运行。
由于GIL的存在,即使是多线程,事实上同一时刻只能保证一个线程在运行,既然这样多线程的运行效率不就和单线程一样了吗,那为什么还要使用多线程呢?
由于以前的电脑基本都是单核CPU,多线程和单线程几乎看不出差别,可是由于计算机的迅速发展,现在的电脑几乎都是多核CPU了,最少也是两个核心数的,这时差别就出来了:通过之前的案例我们已经知道,即使在多核CPU中,多线程同一时刻也只有一个线程在运行,这样不仅不能利用多核CPU的优势,反而由于每个线程在多个CPU上是交替执行的,导致在不同CPU上切换时造成资源的浪费,反而会更慢。即原因是一个进程只存在一把gil锁,当在执行多个线程时,内部会争抢gil锁,这会造成当某一个线程没有抢到锁的时候会让cpu等待,进而不能合理利用多核cpu资源。
但是在使用多线程抓取网页内容时,遇到IO阻塞时,正在执行的线程会暂时释放GIL锁,这时其它线程会利用这个空隙时间,执行自己的代码,因此多线程抓取比单线程抓取性能要好,所以我们还是要使用多线程的。
参考文章:
Python-100-Days/13.进程和线程.md at master · jackfrued/Python-100-Days · GitHub
加载全部内容