您的位置:首页 > 理论基础 > 计算机网络

python网络编程(4)—— 多任务、多线程

2020-07-31 14:29 856 查看

python网络编程(4)—— 多任务

  • 互斥锁
  • 注意事项
  • 介绍

    多件事情同时运行,即多任务。
    在我们的任务管理器中所看到的多个进程同时运行就是多任务情形。

    有顺序的进行任务不是多任务,如先唱歌在跳舞。

    from time import sleep
    def sing():
    for i in range(3):
    print(f'正在唱歌。。。{i}')
    sleep(1)
    
    def dance():
    for i in range(3):
    print(f'正在跳舞。。。{i}')
    sleep(1)
    
    if __name__ == '__main__':
    sing()
    dance()

    让唱歌跳舞同时进行,所用的方法就是多任务。

    import threading
    from time import sleep
    def sing():
    for i in range(3):
    print(f'正在唱歌。。。{i}')
    sleep(1)
    
    def dance():
    for i in range(3):
    print(f'正在跳舞。。。{i}')
    sleep(1)
    
    if __name__ == '__main__':
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()

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

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

    我们平时看到的多任务实际上是cpu在不停地切换执行程序。

    注意:

    • 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
    • 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。

    多线程

    线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    为了提高程序的执行效率,多线程就成了必要。

    python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用

    以下是顺序运行的代码,

    import time
    def saySorry():
    print('亲爱的我错了我可以吃饭了吗?')
    time.sleep(1)
    
    if __name__ == '__main__':
    start = time.time()
    saySorry()
    end = time.time()
    print(f'代码执行耗时{end-start}')
    
    执行结果:
    亲爱的我错了我可以吃饭了吗?
    代码执行耗时1.0002381801605225

    以下是多线程代码:

    import threading
    import time
    def saySorry():
    print('亲爱的我错了我可以吃饭了吗?')
    time.sleep(1)
    
    if __name__ == '__main__':
    start = time.time()
    for i in range(5):
    t = threading.Thread(target=saySorry)
    t.start()
    end = time.time()
    print(f'代码执行耗时{end-start}')
    
    执行结果:
    亲爱的我错了我可以吃饭了吗?
    亲爱的我错了我可以吃饭了吗?
    亲爱的我错了我可以吃饭了吗?
    亲爱的我错了我可以吃饭了吗?
    亲爱的我错了我可以吃饭了吗?
    代码执行耗时0.0010030269622802734

    可以明显看出使用了多线程并发的操作,花费时间要短很多
    当调用

    start()
    时,才会真正的创建线程,并且开始执行

    关于线程执行顺序

    import threading
    from time import sleep,ctime
    
    def sing():
    print(f'开始唱歌。。。{ctime()}')
    sleep(3)
    print(f'结束唱歌。。。{ctime()}')
    
    def dance():
    print(f'开始跳舞。。。{ctime()}')
    sleep(3)
    print(f'结束跳舞。。。{ctime()}')
    
    if __name__ == '__main__':
    print(f'程序开始。。。{ctime()}')
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    
    t2.start()
    while True:
    print(threading.enumerate())
    print(ctime())
    if len(threading.enumerate())<=1:
    break
    sleep(0.5)
    t1.join()
    t2.join()
    print(f'程序结束。。。{ctime()}')

    当我们执行多次多线程程序时,可以看出,多线程程序的执行顺序是不确定的。当我们执行到

    sleep()
    的时候,线程进入阻塞状态,sleep结束后,线程进入就绪状态,等待调度。而线程调度会自主选择一个线程执行,直到所有线程全部执行完成。但线程的执行顺序我们是无法控制的。

    线程之间共享全局变量

    线程无参数:

    g_num = 100
    
    def work1():
    global g_num
    for i in range(10):
    g_num += 1
    print(f'-----------------in work1 g_num={g_num}')
    
    def work2():
    print(f'-----------------in work1 g_num={g_num}')
    
    if __name__ == '__main__':
    print(f'程序开始时,g_num的初始值是{g_num}')
    t1 = threading.Thread(target=work1)
    t1.start()
    t1.join()
    t2 = threading.Thread(target=work2)
    t2.start()
    t2.join()

    线程有参数:

    def work1(lis):
    lis.append(33)
    lis.append(44)
    print(f'-----------------in work1 lis={lis}')
    
    def work2(lis):
    print(f'-----------------in work1 lis={lis}')
    
    if __name__ == '__main__':
    
    lis = [11,22]
    print(f'程序开始时,lis的初始值是{lis}')
    # 如果线程任务需要接收参数
    # 创建线程时,以元组的方式传给args即可
    t1 = threading.Thread(target=work1,args=(lis,))
    t1.start()
    t1.join()
    t2 = threading.Thread(target=work2,args=(lis,))
    t2.start()
    # 线程之间共享全局变量,包括可变和不可变类型

    注意

    线程之间共享全局变量会出现资源紧张的问题。

    g_num = 0
    
    def work1(num):
    global g_num
    for i in range(num):
    g_num += 1
    print(f'-----------------in work2 g_num={g_num}')
    
    def work2(num):
    global g_num
    for i in range(num):
    g_num += 1
    print(f'-----------------in work1 g_num={g_num}')
    
    if __name__ == '__main__':
    print(f'程序开始时,g_num的初始值是{g_num}')
    t1 = threading.Thread(target=work1,args=(1000000,))
    
    t2 = threading.Thread(target=work2,args=(1000000,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(f'程序结束时,g_num的最终值是{g_num}')

    结果按照正常来说应该是200 0000,但实际执行结果却是:

    程序开始时,g_num的初始值是0
    -----------------in work2 g_num=999886
    -----------------in work1 g_num=1145184
    程序结束时,g_num的最终值是1145184

    原因:前面说过,线程执行是无序的,这导致了资源竞争。当线程一拿到数据时本应往下传递,但在传递的时候,线程二还没拿到线程一的数据,由于线程一不再占用资源,于是线程二就开始执行了,导致线程一执行结果即正在传递的数据失效作废,但所有线程还在往下执行。

    这种现象是概率型的,但事件越多概率越大,也就导致了以上执行结果与预想结果偏差过大。

    所以说,线程间是不安全的。

    互斥锁

    为了解决多个线程间共享数据的资源竞争问题,python引进互斥锁。

    g_num = 0
    mutex = threading.Lock() # 创建锁对象
    
    def work1(num):
    global g_num
    for i in range(num):
    mutex.acquire() # 上锁
    g_num += 1
    mutex.release() # 解锁
    print(f'-----------------in work2 g_num={g_num}')
    
    def work2(num):
    global g_num
    for i in range(num):
    mutex.acquire()  # 上锁
    g_num += 1
    mutex.release()  # 解锁
    print(f'-----------------in work1 g_num={g_num}')
    
    if __name__ == '__main__':
    print(f'程序开始时,g_num的初始值是{g_num}')
    t1 = threading.Thread(target=work1,args=(1000000,))
    
    t2 = threading.Thread(target=work2,args=(1000000,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(f'程序结束时,g_num的最终值是{g_num}')

    缺陷:效率变低。可能产生死锁。

    死锁代码:

    m1 = threading.Lock()
    m2 = threading.Lock()
    
    def run1():
    # 对m1加锁
    m1.acquire()
    print('run------------------1')
    time.sleep(1)
    m2.acquire()
    print('run------------------2')
    time.sleep(1)
    m2.release()
    # 对m1解锁
    m1.release()
    
    def run2():
    # 对m2加锁
    m2.acquire()
    print('run------------------2')
    time.sleep(1)
    m1.acquire()
    print('run------------------1')
    time.sleep(1)
    m1.release()
    # 对m2解锁
    m2.release()

    解决死锁办法:
    1、银行家算法

    注意事项

    • join() —— 主线程会等待该县城结束后才会结束。
    • 并行、并发、同步、异步、互斥、阻塞是多线程必须了解的概念。
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: