python进阶之多任务编程(包含进程&线程&协程)

一、多任务编程
1、多任务的概念?
现实⽣活中的多任务:有很多的场景中的事情是同时进⾏的,⽐如开⻋的时候 ⼿和脚共同来驾驶汽⻋,再⽐如唱歌跳舞也是同时进⾏的。

即就是操作系统可以同时运⾏多个任务。打个 ⽐⽅,你⼀边在⽤浏览器上⽹,⼀边在听MP3,⼀边在⽤Word赶作业,这就是多任务,⾄少同时有3个任务正在运⾏。还有很多任务悄悄地在后台同时运 ⾏着,只是桌⾯上没有显示⽽已。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

——答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

2、单核CPU如何实现“多任务?
操作系统轮流让各个任务交替执⾏,每个任务执⾏0.01秒,这样反复执⾏下去。 表⾯上看,每个任务交替执⾏,但CPU的执⾏速度实在是太快了,感觉就像所有任务都在同时执⾏⼀样。

在这里插入图片描述

三个执行实例A,B,C在单个CPU上交替执行
逻辑上表现为三个执行实例并发执行
但实质物理上任然是串行执行

串行:一个处理完再一个
并发和并行:
并发:处理多个任务,不一定同时
例如:你正在吃饭,吃到一半电话响,去接电话,接完后继续吃饭
并行:同时处理多个任务
例如:边吃饭边打电话

3、多核CPU如何实现“多任务?
真正的并⾏执⾏多任务只能在多核CPU上实现,但是,由于任务数量远远多 于CPU的核⼼数量,所以,操作系统也会⾃动把很多任务轮流调度到每个核 ⼼上执⾏。

在这里插入图片描述

二、多进程编程
1、进程的创建
(1)程序和进程的区别
程序:编写完的代码,没有运行
进程:正在运⾏着的代码,需要运⾏的环境等
(2)进程的五状态模型:
在这里插入图片描述

创建(created)-----就绪(ready)-----运行(running)-----阻塞(waiting)-----结束(terminated)

(3)创建子进程

Python的os模块封装了常⻅的系统调⽤,其中就包括fork,可以在Python程 序中轻松创建⼦进程:
Python的os模块中的fork()函数,用来创建子进程,但是只能用在linux系统中。

在这里插入图片描述

fork()函数理解:

执⾏到os.fork()时,操作系统会创建⼀个新的进程复制⽗进程的所有信息到⼦进程中普通的函数调⽤,调⽤⼀次,返回⼀次,但是fork()调⽤⼀次,返回两次⽗进程和⼦进程都会从fork()函数中得到⼀个返回值,⼦进程返回是0,⽽⽗进程中返回⼦进程的 id号

多进程中,每个进程中所有数据(包括全局变量)都各有拥有⼀份,互不影响。

注:(windows 平台下无法使用 os.fork ,IDE 虽然不会报错.但是程序执行起来之后确实无法使用)

"""
多进程中,每个进程中所有数据(包括全局变量)都各拥有⼀份,互不影响
"""
 
import os
import time
 
# 定义一个全局变量money
money = 100
print("当前进程的pid:", os.getpid())
print("当前进程的父进程pid:", os.getppid())
# time.sleep(115)
 
p = os.fork()           #windows 平台下无法使用  os.fork ,IDE 虽然不会报错.但是程序执行起来之后确实无法使用
# 子进程返回的是0
if p == 0:
    money = 200
    print("子进程返回的信息, money=%d" % (money))
# 父进程返回的是子进程的pid
else:
    print("创建子进程%s, 父进程是%d" % (p, os.getppid()))
    print(money)

2、多进程编程
Windows没有fork调⽤,由于Python是跨平台的, multiprocessing模块就是跨平台版本 的多进程模块。
multiprocessing模块提供了⼀个Process类来代表⼀个进程对象。

Process([group [, target [, name [, args [, kwargs]]]]])
target:表示这个进程实例所调⽤对象;就是进程要执行的内容
args:表示调⽤对象的位置参数元组;
kwargs:表示调⽤对象的关键字参数字典;
name:为当前进程实例的别名;
group:⼤多数情况下⽤不到;

Process类常⽤⽅法:
is_alive() : 判断进程实例是否还在执⾏;
join([timeout]) : 是否等待进程实例执⾏结束,或等待多少秒;
start() : 启动进程实例(创建⼦进程);
run() : 如果没有给定target参数,对这个对象调⽤start()⽅法时,就将执 ⾏对象中的run()⽅法;
terminate() : 不管任务是否完成,⽴即终⽌;

Process类常⽤属性:
name:当前进程实例别名,默认Process-N,N为从1开始计数;
pid:当前进程实例的PID值

多进程编程方法一:实例化对象

from multiprocessing import Process
import time
 
 
def task1():
    print("正在听音乐")
    time.sleep(1)
 
def task2():
    print("正在编程......")
    time.sleep(0.5)
 
def no_multi():
    task1()
    task2()
 
def use_multi():
    p1 = Process(target=task1)
    p2 = Process(target=task2)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
 
    # p.join()  阻塞当前进程, 当p1.start()之后, p1就提示主进程, 需要等待p1进程执行结束才能向下执行, 那么主进程就乖乖等着, 自然不会执行p2.start()
    # [process.join() for process in processes]
 
if __name__ == '__main__':
    # 主进程
    start_time= time.time()
    # no_multi()
    use_multi()
    end_time = time.time()
    print(end_time-start_time)

运行结果:

正在听音乐
正在编程......
1.1266577243804932

注意:对于代码中join() 方法的使用:

在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源。
如果主进程的任务在执行到某一个阶段时,需要等待子进程完毕后才能继续执行,就需要有一种机制能够让主进程检测子进程是否运行完毕。
在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是join方法的作用。
join()的作用:在进程中可以阻塞主进程的执行, 直到等待子线程全部完成之后, 才继续运行主线程后面的代码。

def use_multi():
    p1 = Process(target=task1)
    p2 = Process(target=task2)
    p1.start()
    #p1.join()    # 如果写在这里,就是执行一个等待一个,
                  # 等p1进程结束后,才能向下执行,也就是相当于一条一条执行,没有用到多进程
    p2.start()
    p1.join()     # 等待p1进程执行结束
    p2.join()     # 等待p2进程执行结束
                  # 其实此时的p1,p2是在同时进行,他俩都start开启了,等待他们结束的时间是最长进程的那个时间。
                  # 就不是在一个一个等了,一起等。

多进程编程方法二:创建子类(继承的式)
说白了即就是重写run() 方法,run()方法里就是要执行的任务

"""
创建子类, 继承的方式
"""
from multiprocessing import Process
import time
 
class MyProcess(Process):
    """
    创建自己的进程, 父类是Process
    """
 
    def __init__(self, music_name):
        super(MyProcess, self).__init__()
        self.music_name = music_name
 
    def run(self):
        """重写run方法, 内容是你要执行的任务"""
 
        print("听音乐%s" % (self.music_name))
        time.sleep(1)
 
# 开启进程: p.start()  ====== p.run()
if __name__ == '__main__':
    for i in range(10):
        p = MyProcess("音乐%d" % (i))
        p.start()

运行结果:

听音乐音乐0
听音乐音乐1
听音乐音乐2
听音乐音乐3
听音乐音乐4
听音乐音乐5
听音乐音乐6
听音乐音乐7
听音乐音乐8
听音乐音乐9

多进程编程方法三:使用进程池Pool

1)为什么需要进程池:

当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程, 十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。
Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果池还没有满, 那么就会创建一个新的进程用来执行该请求;
但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。

在这里插入图片描述
进程池的使用方法:
from multiprocessing import Pool
from multiprocessing import cpu_count
p = Pool(cpu_count()) #cpu_count() 计算几核CPU
p.map(task,list())
p.close() # 关闭进程池对象
p.join()

如下案例:实现求1000—1200之间的素数:

代码如下:

from multiprocessing import Process
import time
 
# 判断1000~ 1200之间的素数
 
 
def is_prime(num):
    if num == 1:
        return True
    for i in range(2,num):
        if num % i == 0:
            return False
        else:
            return True
 
def task(num):
    if is_prime(num):
        print('%d是素数' %(num))
 
def no_multi():
    for num in range(1000,1201):
        task(num)
 
def use_multi():
    processes = []
    for num in range(1000,1201):
        # 实例化子进程对象
        p = Process(target=task,args=(num,)) # 这个任务需要传参数,args是元组
        #开启子进程
        p.start()
        #存储所有的子进程
        processes.append(p)
        #阻塞子进程,等待所有子进程执行结束
        for process in processes:
            process.join()
 
def use_pool():
    from multiprocessing import Pool
    from multiprocessing import cpu_count
    p = Pool(cpu_count())
    p.map(task,list(range(1000,1201)))
    p.close()  # 关闭进程池对象
    p.join()
 
 
if __name__ == '__main__':
    start_time = time.time()
    #no_multi()    #  0.0009970664978027344
    #use_multi()   #  16.060956239700317 反而用的时间长了
    use_pool()     #  0.26380443572998047  当数据量非常非常大的时候,还是用pool节省时间
    end_time = time.time()
    print(end_time - start_time)
    """
    使用多进程反而用的时间长了
    因为开启的进程太多,我只有4核4个cpu,这样分配给每个cpu的也很多了,
    还要进行复制那么多,所以其实效率没有那么高
    
    不要开启太多进程,创建子进程会耗费时间和空间(内存空间)
    """

运行结果如下:

1001是素数
1003是素数
1005是素数
1007是素数
1009是素数
1011是素数
1013是素数
1015是素数
1017是素数
1019是素数
1021是素数
1023是素数
1025是素数
1027是素数
1029是素数
1031是素数
1033是素数
1035是素数
1037是素数
1039是素数
1041是素数
1043是素数
1045是素数
1047是素数
1049是素数
1051是素数
1053是素数
1055是素数
1057是素数
1059是素数
1061是素数
1063是素数
1065是素数
1067是素数
1069是素数
1071是素数
1073是素数
1075是素数
1077是素数
1079是素数
1081是素数
1083是素数
1085是素数
1087是素数
1089是素数
1091是素数
1093是素数
1095是素数
1097是素数
1099是素数
1101是素数
1103是素数
1105是素数
1107是素数
1109是素数
1111是素数
1113是素数
1115是素数
1117是素数
1119是素数
1121是素数
1123是素数
1125是素数
1127是素数
1129是素数
1131是素数
1133是素数
1135是素数
1137是素数
1139是素数
1141是素数
1143是素数
1145是素数
1147是素数
1149是素数
1151是素数
1153是素数
1155是素数
1157是素数
1159是素数
1161是素数
1163是素数
1165是素数
1167是素数
1169是素数
1171是素数
1173是素数
1175是素数
1177是素数
1179是素数
1181是素数
1183是素数
1185是素数
1187是素数
1189是素数
1191是素数
1193是素数
1195是素数
1197是素数
1199是素数
0.6310694217681885

3、进程间通信
(1)进程间通信的目的:
在这里插入图片描述
数据传输、共享数据、通知事件、资源共享、进程控制
(2)进程间通信的方式:
在这里插入图片描述
管道、信号、信号量、消息队列、套接字

消息队列:

可以使⽤multiprocessing模块的Queue实现多进程之间的数据传递,Queue 本身是 ⼀个消息列队程序

Queue.qsize() : 返回当前队列包含的消息数量;
Queue.empty() : 如果队列为空,返回True,反之False ;
Queue.full() : 如果队列满了,返回True,反之False

Queue.get([block[, timeout]]) :
出队,block默认值为True(阻塞,如果没有数据,一直等待,等到有数据了,出队)
Queue.get_nowait() : 相当Queue.get(False)(不阻塞)

Queue.put(item,[block[, timeout]]) :
⼊队,block默认值 为True(如果队列满了不能入队,一直等待,等待有位置可以入队了就入队)
Queue.put_nowait(item) : 相当Queue.put(item, False)

如下:(完成进程A与进程B之间的9次通信)

from multiprocessing import Process
from multiprocessing import Queue
 
import time
 
class Producer(Process):
    def __init__(self,queue):
        super(Producer, self).__init__()
        self.queue = queue
 
    def run(self):
        """将需要通信的数据写入队列中"""
        for i in range(10):
            self.queue.put(i)
            time.sleep(0.1)
            print('进程A向进程B传递信息,内容为%s' %(i))
 
class Consumer(Process):
    def __init__(self,queue):
        super(Consumer, self).__init__()
        self.queue = queue
 
    def run(self):
        while True:
            time.sleep(0.1)
            data = self.queue.get()
            print('进程B接收到进程A传递的信息:',data)
 
if __name__ == '__main__':
    queue = Queue()
    p1 = Producer(queue)
    c1 = Consumer(queue)
 
    p1.start()
    c1.start()
    p1.join()
    c1.join()

运行结果如下:

进程A向进程B传递信息,内容为0
进程B接收到进程A传递的信息: 0
进程A向进程B传递信息,内容为1
进程B接收到进程A传递的信息: 1
进程A向进程B传递信息,内容为2
进程B接收到进程A传递的信息: 2
进程A向进程B传递信息,内容为3
进程B接收到进程A传递的信息: 3
进程A向进程B传递信息,内容为4
进程B接收到进程A传递的信息: 4
进程A向进程B传递信息,内容为5
进程B接收到进程A传递的信息: 5
进程A向进程B传递信息,内容为6
进程B接收到进程A传递的信息: 6
进程A向进程B传递信息,内容为7
进程B接收到进程A传递的信息: 7
进程A向进程B传递信息,内容为8
进程B接收到进程A传递的信息: 8
进程A向进程B传递信息,内容为9
进程B接收到进程A传递的信息: 9

三、多线程编程
1、什么是线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
2、适用范围
1.服务器中的文件管理或通信控制

2.前后台处理

3.异步处理

3、特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。

1)轻型实体

线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。

线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:

(1)线程状态。

(2)当线程不运行时,被保存的现场资源。

(3)一组执行堆栈。

(4)存放每个线程的局部变量主存区。

(5)访问同一个进程中的主存和其它资源。

用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

2)独立调度和分派的基本单位。

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

3)可并发执行。

在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

4)共享进程资源。

在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

在这里插入图片描述

每个进程至少有一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。
在这里插入图片描述

线程的几种状态如下图:
在这里插入图片描述
4、线程与进程的区别
进程是资源分配的最小单位,线程是程序执行的最小单位。
进程有自己的独立地址空间。线程是共享进程中的数据的,使用相同的地址空间。
进程之间的通信需要以通信的方式(IPC)进行。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,难点:处理好同步与互斥。
线程分类: 内核线程、用户线程或用户空间线程
内核线程是操作系统的一部分,而内核中没有实现用户空间线程

5、多线程编程
跟多进程类似,python的thread模块是⽐较底层的模块,python的threading 模块是对thread做了⼀些 包装的,可以更加⽅便的被使⽤

例如:

import threading
 
if __name__ == '__main__':
    # 一个进程中一定有一个线程,叫主线程,用来管理其他线程
    print('当前线程个数:',threading.active_count()) #运行这个程序就是一个进程,一个进程中肯定有一个线程
    print('当前线程信息:',threading.current_thread())

运行结果:

当前线程个数: 1
当前线程信息: <_MainThread(MainThread, started 6252)>

多线程编程方法一:实例化对象

import time
import threading
 
def task():
    """当前要执行的任务"""
    print('听音乐。。。')
    time.sleep(1)
 
if __name__ == '__main__':
    start_time = time.time()
 
    threads = []
    for count in range(5):
        t = threading.Thread(target=task)
        t.start()
        threads.append(t)
    # 等待所有的子线程执行结束
    for thread in threads:
        thread.join()
 
    end_time = time.time()
    print(end_time - start_time

运行结果:

听音乐。。。
听音乐。。。
听音乐。。。
听音乐。。。
听音乐。。。
1.002403736114502

项目案例:(IP地址归属地批量查询任务)

import requests
import json
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread
 
 
def task(ip):
    """获取指定IP的所在城市和国家并存储到数据库中"""
    # 获取网址的返回内容
    url = 'http://ip-api.com/json/%s' % (ip)
    try:
        response = requests.get(url)
    except Exception as e:
        print("网页获取错误:", e)
    else:
        # 默认返回的是字符串
        """
        {"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
        """
        contentPage = response.text
        # 将页面的json字符串转换成便于处理的字典;
        data_dict = json.loads(contentPage)
        # 获取对应的城市和国家
        city = data_dict.get('city', 'null')  # None
        country = data_dict.get('country', 'null')
 
        print(ip, city, country)
        # 存储到数据库表中ips
        ipObj = IP(ip=ip, city=city, country=country)
        session.add(ipObj)
        session.commit()
 
 
if __name__ == '__main__':
    engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql",
                           encoding='utf8',
                           # echo=True
                           )
    # 创建缓存对象
    Session = sessionmaker(bind=engine)
    session = Session()
 
    # 声明基类
    Base = declarative_base()
 
 
    class IP(Base):
        __tablename__ = 'ips'
        id = Column(Integer, primary_key=True, autoincrement=True)
        ip = Column(String(20), nullable=False)
        city = Column(String(30))
        country = Column(String(30))
 
        def __repr__(self):
            return self.ip
 
 
    # 创建数据表
    Base.metadata.create_all(engine)
 
    # 1.1.1.1 -- 1.1.1.10
    threads = []
    for item in range(10):
        ip = '1.1.1.' + str(item + 1)  # 1.1.1.1 -1.1.1.10
        # task(ip)
        # 多线程执行任务
        thread = Thread(target=task, args=(ip,))
        # 启动线程并执行任务
        thread.start()
        # 存储创建的所有线程对象;
        threads.append(thread)
 
    [thread.join() for thread in threads]
    print("任务执行结束.........")
    print(session.query(IP).all())

分析:多线程程序的执⾏顺序是不确定的。 当执⾏到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进⼊就绪(Runnable)状态,等待调度。⽽线程调度将⾃⾏选择⼀个线程执⾏。 代码中只能保证每个线程都运⾏完整个run函数,但是线程的启动顺序、 run函数中每次循环的执⾏顺序都不能确定。

多线程编程方法二:创建子类

项目案例:(项目案例: 基于多线程的批量主机存活探测)

项目描述: 如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个os.popen(“ping -q -c2”+ ip)。 项目瓶颈: 没有线程的解决方案效率非常低,因为脚本必须等待每次ping。


import os
from threading import Thread
 
class GetHostAliveThread(Thread):
    """
    创建子线程,执行的任务:判断指定的IP是否存活
    """
    def __init__(self,ip):
        super(GetHostAliveThread, self).__init__()
        self.ip = ip
 
    def run(self):
        """
        重写run()方法:判断指定的IP是否存活
        #执行shell命令行语句
        os.system()  返回值为0:命令正确执行,不报错  返回值不为0:执行报错
        os.system('ping -c1 -w1 172.25.254.49 &> /dev/null')  # &> /dev/null 不显示信息
        0
        os.system('ping -c1 -w1 172.25.254.1 &> /dev/null')
        256
        """
 
        # 执行的shell命令
        cmd = 'ping -c1 -w1 %s ' %(self.ip)
        result = os.system(cmd)
        if result != 0:
            print('%s主机没有ping通' %(self.ip))
 
if __name__ == '__main__':
    print('打印172.25.254.0网段没有使用的IP地址'.center(10,'*'))
    for i in range(1,255):
        ip = '172.25.254.' + str(i)
        thread = GetHostAliveThread(ip)
        thread.start()

6、GIL全局解释器锁
1)共享全局变量

优点: 在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提 下完成多线程之间的数据共享(这点要⽐多进程要好)

缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)
在这里插入图片描述
2)如何解决线程不安全问题

GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行;

Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

在这里插入图片描述

7、线程同步和线程锁
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作.

同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。 "同"字从字⾯上容易理解为⼀起动作 其实不是, "同"字应是指协同、协助、互相配合。
在这里插入图片描述

from threading import Thread
 
money = 0
 
def add():
    for i in range(1000000):
        global money
        money += 1
 
def reduce():
    for i in range(1000000):
        global money
        money -= 1
 
if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=reduce)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(money)

运行结果:

289914

并不是我们想要的0
这是因为在数据量非常大的时候,在运算的过程中,多个线程可能会出现不同步的现象
一开始money=0
当第一个线程add来的时候
money += 1 ,money只是加了1,这个时候还没有将1赋给money,money仍然是0,
这时候第二个线程reduce来了,他直接在money还是0的基础上就去减1,因此结果并不是我们想要的

这是因为多线程共享全局变量
线程对全局变量随意遂改可能造成多线程之间对全局变量的混乱

因此,在对数据进行更改时怎么实现线程同步?
需要用到线程锁:

线程锁:多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!so,不使用线程锁, 可能导致错误。

线程锁的实现如下:

#实例化一个锁对象,主函数中
lock = threading.Lock()

#操作变量之前进行加锁
lock.acquire()
#操作变量之后进行解锁
lock.release()

from threading import Thread
from threading import Lock
 
 
money = 0
 
def add():
    for i in range(1000000):
        global money
 
        #操作变量之前进行加锁
        lock.acquire()
        money += 1
        #操作变量之后进行解锁
        lock.release()
 
def reduce():
    for i in range(1000000):
        global money
 
        # 操作变量之前进行加锁
        lock.acquire()
        money -= 1
        # 操作变量之后进行解锁
        lock.release()
 
if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=reduce)
    #实例化一个线程锁
    lock = Lock()
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(money)

运行结果:

0

8、死锁
1)定义:

在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统的处理能力。然而,多个进程的并发执行也带来了新的问题——死锁。

所谓死锁是指多个进程因竞争资源而造成的一种僵局,若无外力作用,这些进程都将无法向前推进。
在这里插入图片描述
在这里插入图片描述

比如呢就是:A有1个苹果1个手机,B有1个梨1个电脑
A说:你先把你的梨给我,我就把我的苹果给你
但是B说:你得先把你的手机给我,我就把我的电脑给你
他俩都在等待对方给资源,这样造成的情况就可以简单理解为死锁。

如下代码:

"""转账操作"""
 
import time
import threading
 
class Account(object):
    def __init__(self, id, money, lock):
        self.id = id
        self.money = money
        self.lock = lock
 
    def reduce(self, money):
        self.money -= money
 
    def add(self, money):
        self.money += money
 
def transfer(_from, to, money):
    if _from.lock.acquire():  # 加锁
        _from.reduce(money)
        #让线程之间有一个争夺资源的过程,有一点时间的浪费
        time.sleep(1)
        
        if to.lock.acquire(): # 加锁
            to.add(money)
            to.lock.release()
        _from.lock.release()
 
if __name__ == '__main__':
    a = Account('a',1000,threading.Lock())
    b = Account('b',1000,threading.Lock())
 
    t1 = threading.Thread(target=transfer, args=(a, b, 200))
    t2 = threading.Thread(target=transfer, args=(b, a, 100))
    t1.start()
    t2.start()
    print(a.money)
    print(b.money)

运行结果:

800
900
在这里插入代码片

2)产生死锁的原因

竞争系统资源
进程运行推进的顺序不当
资源分配不当
3)产生死锁的四个必要条件

互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
4)解决死锁的办法

1.减少资源占用时间,可以降低死锁放生的概率。
2.银行家算法。银行家算法的本质是优先满足占用资源较少的任务。
3.理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。

四、协程
1、什么是协程
协程,又称微线程,纤程。英文名Coroutine。协程看上去也是子程序,但执行过程中, 在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行
在这里插入图片描述

进程 — 线程 — 协程
可以对应理解为:程序 — 函数 — 函数片段

在执行到一个函数的某个语句时,跳到另外一个函数的某个语句去执行,执行一会儿后又跳回到原来停止的地方执行

和yield类似,可以通过yield去实现协程

2、协程的优势
(1)执行效率极高,因为子程序切换(函数),不是线程切换,由程序自身控制,
(2)没有切换线程的开销。所以与多线程相比,线程的数量越多,协程性能的优势越明显。
(3)不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁,因此执行效率高很多

3、协程的实现
1)方法一: yield实现


import time
 
def consumer():
    r = ''
    while True:
        n = yield r    # 停止
        if not n :
            return
        print('[consumer] Consuming %s...' %(n))
        time.sleep(1)
        r = '200 ok'
 
def produce(c):
    c.next()   # 执行consumer生成器内容
    n = 0
    while n < 5:
        n = n + 1
        print('[producer] Produceing %s...' %(n))
        r = c.send(n)  # 把n=1发送给consumer 15行
        print('[producer] Consumer rerurn: %s' %(r))
    c.close()
 
if __name__ == '__main__':
    c = consumer()  # consumer()函数里有yield ,所以返回的c是一个生成器
    produce(c)

方法二:gevent模块实现

基本思想: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到 IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO

gevents = [gevent.spawn()]
gevent.joinall(gevents)

如下:

import gevent
 
import requests
import json
 
from gevent import monkey
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread
 
from gevent import monkey
# 打补丁
monkey.patch_all()
 
 
def task(ip):
    """获取指定IP的所在城市和国家并存储到数据库中"""
    # 获取网址的返回内容
    url = 'http://ip-api.com/json/%s' % (ip)
    try:
        response = requests.get(url)
    except Exception as e:
        print("网页获取错误:", e)
    else:
        # 默认返回的是字符串
        """
        {"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
        """
        contentPage = response.text
        # 将页面的json字符串转换成便于处理的字典;
        data_dict = json.loads(contentPage)
        # 获取对应的城市和国家
        city = data_dict.get('city', 'null')  # None
        country = data_dict.get('country', 'null')
 
        print(ip, city, country)
        # 存储到数据库表中ips
        ipObj = IP(ip=ip, city=city, country=country)
        session.add(ipObj)
        session.commit()
 
 
if __name__ == '__main__':
    engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql",
                           encoding='utf8',
                           # echo=True
                           )
    # 创建缓存对象
    Session = sessionmaker(bind=engine)
    session = Session()
 
    # 声明基类
    Base = declarative_base()
 
 
    class IP(Base):
        __tablename__ = 'ips'
        id = Column(Integer, primary_key=True, autoincrement=True)
        ip = Column(String(20), nullable=False)
        city = Column(String(30))
        country = Column(String(30))
 
        def __repr__(self):
            return self.ip
 
 
    # 创建数据表
    Base.metadata.create_all(engine)
 
    # 使用协程
    gevents = [gevent.spawn(task, '1.1.1.' + str(ip + 1)) for ip in range(10)]
    gevent.joinall(gevents)
    print("执行结束....")

五、总结
在这里插入图片描述

多任务分类:
(1)IO密集型的任务
有阻塞状态,需要等待,不会一直占用CPU
建议使用多线程编程
(2)计算密集型的任务
没有阻塞状态,一直占用CPU
建议使用多进程编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值