线程与进程(一)

226 阅读6分钟
  • 进程是资源分配的最小单位 ,是一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在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()

结果:

image.png 以上就是用两个函数来模拟不同的任务,并且从执行结果来看,是挨个执行的,这种执行方式则是串行

多线程的创建

通过函数来创建

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()

输出:

image.png

这时候我们就可以加上线程锁

以上更改之后的代码:

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)

上锁之后,程序就会从上往下按照顺序来执行,并且是执行完第一个之后才会执行第二个。

锁的好处

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能由单线程模式执行,降低了效率
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方的锁的时候,可能会造成死锁