- 进程是
资源分配的最小单位,是一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。 - 线程(thread)是
CPU调度的最小单位,是进程中的一个执行任务(控制单元),负责当前进程中程序的执行 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源 没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
多进程
进程可以简单的理解为一个可以独立运行的程序单位,它是线程的集合,线程是进程中实际运行的单位
多进程就是指计算机同时执行多个进程,即操作系统可以同时打开多个程序,手机可以同时运行多个程序
多线程
线程是进程中的一个执行单元,多线程即一个进程中 拥有多个执行单元。例如浏览器可以同时打开多个网页,可以一边听音乐,一边浏览网页
多线程爬虫
由于外部网络不稳定,在使用单线程爬取网页数据时,如果有一个网页响应速度慢或者卡住了,那整个程序都要等待下去,这显然是无效率的。因此,我们可以使用多线程、多进程、协程技术来实现并发下载网页。
一般来说,多进程适用于CPU密集型的代码,例如各种循环处理、大量的密集并行计算等。多线程适用于I/O密集型的代码,例如文件处理、网络交互等。协程无需通过操作系统调度,没有进程、线程之间的切换和创建等开销,适用于大量不需要CPU的操作,例如网络I/O等。
实际上,限制爬虫程序发展的瓶颈就在于网络I/O,原因是网络I/O的速度赶不上CPU的处理速度。结合多线程、多进程和协程的特点和用途,我们一般采用多线程和协程技术来实现爬虫程序。
程序中模拟多任务
import time
def sing():
for i in range(3):
print("正在唱歌... %d"%i)
time.sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
time.sleep(1)
if __name__ == '__main__':
sing()
dance()
结果:
以上就是用两个函数来模拟不同的任务,并且从执行结果来看,是挨个执行的,这种执行方式则是
串行。
多线程的创建
通过函数来创建
import time
import threading
def sing():
for i in range(3):
print("正在唱歌... %d"%i)
time.sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
通过类来创建
class Test_thread(threading.Thread):
def run(self):
for i in range(5):
print('hello 子线程')
主线程和子线程的执行关系
- 主线程会等待子线程结束之后再结束
- join()等待子线程结束之后,主线程再继续执行
- .daemon=True()守护线程,
def Demo01():
for i in range(5):
print('hello 子线程')
time.sleep(0.5)
if __name__ == "__main__":
t = threading.Thread(target=Demo01)
# t.daemon = True # 守护线程,不会等待子线程结束
t.start()
# 第一种sleep(5秒)
t.join() # 等待子线程结束之后,主线程继续执行
print(123)
查看线程数量
import threading
import time
def demo1():
for i in range(5):
print(f"demo1---{i}")
time.sleep(1)
def demo2():
for i in range(10): # 作为区别将5改成10
print(f"demo2---{i}")
time.sleep(1)
def main():
t1 = threading.Thread(target=demo1)
t2 = threading.Thread(target=demo2)
t1.start()
t2.start()
# print(threading.enumerate()) # 线程是存活的
while True:
print(threading.enumerate())
if len(threading.enumerate()) <= 1:
break
time.sleep(1)
if __name__ == '__main__':
main()
资源竞争
如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
from threading import Thread
import time
import threading
g_num = 0
def work1():
global g_num
for i in range(10000000):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2():
global g_num
for i in range(10000000):
g_num += 1
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = Thread(target=work1)
t2 = Thread(target=work2)
t1.start()
t2.start()
输出:
这时候我们就可以加上线程锁
以上更改之后的代码:
from threading import Thread
import time
import threading
g_num = 0
lock = threading.Lock() # 全局锁
def work1():
lock.acquire() # 上锁
global g_num
for i in range(10000000):
g_num += 1
lock.release() # 解锁
print("----in work1, g_num is %d---"%g_num)
def work2():
lock.acquire() # 上锁
global g_num
for i in range(10000000):
g_num += 1
lock.release() # 解锁
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = Thread(target=work1)
t2 = Thread(target=work2)
t1.start()
t2.start()
线程锁
对于多线程中由于资源竞争从而导致计算错误的问题,可以通过线程同步来进行解决。 同步就是协同步调,按预定的先后次序进行运行,"同"字从字面上容易理解为一起动作,其实不是,"同"字应是指协同、协助、互相配合。 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定,threading模块中定义了Lock类,可以方便的处理锁定:
import threading
import time
g_num = 0
def test1(num):
global g_num
mutex.acquire() # 上锁
for i in range(num):
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
mutex.acquire() # 上锁
for i in range(num):
g_num += 1
mutex.release() # 解锁
print("---test2---g_num=%d"%g_num)
上锁之后,程序就会从上往下按照顺序来执行,并且是执行完第一个之后才会执行第二个。
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能由单线程模式执行,降低了效率
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方的锁的时候,可能会造成死锁