python全栈(一)网络通信与服务器之多任务-线程

本文详细介绍了Python中的多任务实现,特别是通过线程进行任务并发,包括线程启动、通信、共享全局变量及资源竞争的处理,如互斥锁和死锁防范。通过实例演示了如何使用线程和互斥锁构建UDP聊天器,以展示多任务在实际场景中的应用。
摘要由CSDN通过智能技术生成

前言

本系列博文《网络通信与服务器》主要内容:udp、tcp、tcp下载文件案例、多任务、http协议、网络通信等 。本篇博文主要讲解多任务中的线程。

一、多任务的介绍

有很多的场景中的事情是同时进行的,比如开车的时候手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的。
什么叫多任务:
一个程序里面又多个while True,甚至是多个函数的地方,多个部分不一样的代码,想让他们一起执行,或者有一定规律的执行。
多任务要么进程要么线程。

程序中模拟多任务:
01-没有多任务的程序

import time

def sing():
    """唱歌 5秒钟"""
    for i in range(5):
        print("----正在唱:菊花茶----")
        time.sleep(1)

def dance():
    """跳舞 5秒钟"""
    for i in range(5):
        print("----正在跳舞----")
        time.sleep(1)

def main():
    sing()
    dance()

if __name__ == "__main__":
    main()

运行结果:

----正在唱:菊花茶----
----正在唱:菊花茶----
----正在唱:菊花茶----
----正在唱:菊花茶----
----正在唱:菊花茶----
----正在跳舞----
----正在跳舞----
----正在跳舞----
----正在跳舞----
----正在跳舞----

整个代码执行时间为10秒,两个函数执行是按照先后顺序执行的,不能实现同时执行,此时需要多任务,可以使多个任务同时进行。

二、多任务-线程

多任务是指多个任务同时进行
在实际应用中,多个应用“同时”运行时,并不是真正地同时运行,而是按照时间片轮转的原理进行的。
并发:假的多任务,CPU数小于当前执行的任务数;
并行:真的多任务,CPU数大于等于当前执行的任务数。

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

02-多任务-线程-demo

import time
import threading

def sing():
    """唱歌 5秒钟"""
    for i in range(5):
        print("----正在唱:菊花茶----")
        time.sleep(1)

def dance():
    """跳舞 5秒钟"""
    for i in range(5):
        print("----正在跳舞----")
        time.sleep(1)

def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()
    t2.start()

if __name__ == "__main__":
    main()

分析

  1. 可以明显看出使用了多线程并发的操作,花费时间要很多
  2. 当调用start()时,才会真正的创建线程,并且开始执行
  3. 主线程会等待所有的子线程结束后才结束。

进一步改进–主线程等待子线程结束之后再继续运行:

import threading
import time


def test():
    for i in range(5):
        print('Hello World')
        time.sleep(1)


def main():
    t = threading.Thread(target=test)
    t.start()
    # 等待子线程执行结束,主线程继续向下运行
    t.join()
    print('Main Thread')

if __name__ == '__main__':
    main()

分析:
**代码中添加t.join()**意味着等待子线程执行结束,主线程继续向下运行。

03-让某些线程先执行

import threading
import time

def test1():
    for i in range(5):
        print("-----test1---%d---" % i)

def test2():
    for i in range(5):
        print("-----test2---%d---" % i)

def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start() # 当调用start()时,才会真正的创建线程,并且开始执行

    time.sleep(1)
    print("---1---")

    t2.start()

    time.sleep(1)
    print("---2---")

    print(threading.enumerate())

if __name__ == "__main__":
    main()

04-循环查看当前运行的线程
len(threading.enumerate())

import threading
import time

def test1():
    for i in range(5):
        print("-----test1---%d---" % i)
        time.sleep(1)
        
    # 如果创建Thread时执行的函数,运行结束那么意味着这个子线程结束了....

def test2():
    for i in range(10):
        print("-----test2---%d---" % i)
        time.sleep(1)

def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start()
    t2.start()

    while True:
        print(threading.enumerate())
        if len(threading.enumerate())<=1:
            break
        time.sleep(1)

if __name__ == "__main__":
    main()

05-验证创建线程以及运行时间

注意点:
当调用Thread的时候,不会创建线程;
当调用Thread创建出来的实例对象的start方法的时候,才会创建线程以及让这个线程开始运行

import threading
import time

def test1():
    for i in range(5):
        print("-----test1---%d---" % i)
        time.sleep(1)

def main():
    # 在调用Thread之前先打印当前线程信息
    print(threading.enumerate())
    t1 = threading.Thread(target=test1)

    # 在调用Thread之后打印
    print(threading.enumerate())

    t1.start()

    # 在调用start之后打印
    print(threading.enumerate())

if __name__ == "__main__":
    main()

三、线程间通信和传参

1. 线程间通信

多线程共享全局变量又称为线程间通信。

06-函数里面修改全局变量

num = 100
nums = [11, 22]

def test():
    global num
    num += 100

def test2():
    nums.append(33)

print(num)
print(nums)

test()
test2()

print(num)
print(nums)

说明:多线程之间共享全局变量
global使用的注意点
在一个函数中对全局变量进行修改的时候,到底是否需要使用global进行说明要看是否对全局变量的执行指向进行了修改;
如果修改了执行,即让全局变量指向了一个新的地方,那么必须进行使用global;
如果仅仅是修改了指向的空间中的数据,此时不用必须使用global。
比如:字符串,元组等是不可以修改的,一旦修改,肯定是修改了指向,所以要使用global;而list是可以修改的,所以可以不使用global。
易知,数值型变量要想在函数内部改变,必须声明global;
列表在函数内部通过append()函数改变时,可以不声明global,但是要想通过直接相加的方式改变,必须要声明global。
解释
列表通过直接相加的方式会改变原列表的指向,即原列表增加元素后会成为新的列表;
通过append()方法改变列表的元素后不改变原列表的指向,即还是原来的列表对象。
从而可得
在函数中是否需要加global来改变全局变量,需要看该变量的指向是否改变,如果改变,则需要加global,反之,如果未改变指向、仅仅是修改了指向的空间中的数据,则不需要加global。
从而也就回答了问题:
在函数内部修改全局变量不一定要声明global,要看是否对全局变量的指向进行了修改。
在这里插入图片描述
07-线程共享全局变量

import threading
import time

# 定义一个全局变量
g_num = 100

def test1():
    global g_num
    g_num += 1
    print("-----in test1 g_num=%d----" % g_num)

def test2():
    print("-----in test2 g_num=%d=----" % g_num)

def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start()
    time.sleep(1)

    t2.start()
    time.sleep(1)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()

2. 多线程参数args

08-多线程共享全局变量-2

import threading
import time

def test1(temp):
    temp.append(33)
    print("-----in test1 temp=%s----" % str(temp))

def test2(temp):
    print("-----in test2 temp=%s----" % str(temp))

g_nums = [11, 22]

def main():
    # target指定将来 这个线程去哪个函数执行代码
    # args指定将来调用 函数的时候 传递什么数据过去
    t1 = threading.Thread(target=test1, args=(g_nums,))
    t2 = threading.Thread(target=test2, args=(g_nums,))

    t1.start()
    time.sleep(1)

    t2.start()
    time.sleep(1)

    print("-----in main Thread g_nums = %s---" % str(g_nums))

if __name__ == "__main__":
    main()

四、共享全局变量资源竞争

一个线程写入、一个线程读取没问题,如果有两个线程对某一资源同时写入时,就可能会产生资源竞争。因为线程执行顺序不确定。

09-共享全局变量的问题-资源竞争

import threading
import time

# 定义一个全局变量
g_num = 0

def test1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-----in test1 g_num=%d----" % g_num)

def test2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("-----in test2 g_num=%d=----" % g_num)

def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕....
    time.sleep(5)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()

以上代码运行过程解释
num为全局变量,最开始为0,在test1()、test2()两个子线程中有for循环,在每个子线程的循环中:
(1)先获取num的值;
(2)再把获取到的num加1;
(3)并把结果保存到num。
当执行时,在一个子线程执行到第(2)步时,还未保存,此时轮转到另一个子线程,此时由于第一个子线程还未执行到第(3)步,为保存num,所以num的值为0,执行到第(2)步,也加1,此时回到第一个子线程执行第(3)步保存num,为1,再到第二个子线程执行第三步也保存,所以也为1。
上述现象是一个概率性问题,当循环次数越多时,发生的概率越大,因此循环次数为100时,未发生此现象,循环次数为1000000时,有较明显的现象。
此时即发生了资源竞争

五、互斥锁和死锁

1. 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制;
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥索。
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

函数
mutex = threading.Lock() # 创建一个互斥锁,默认是没有上锁的
mutex.acquire() # 加锁
mutex.release() # 解锁

10-使用互斥锁解决资源竞争的问题

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("-----in 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("-----in test2 g_num=%d=----" % g_num)

# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()

def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕....
    time.sleep(2)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()

运行结果:
此时,子线程1和子线程2的打印正常,但是主线程的打印结果仍然无规律。
运行过程分析:
执行t1.start()和t2.start()后,两个子线程运行,此时会继续向下执行,主线程会打印此时num的值,由于子线程仍可能未运行结束,即num的值未完成相加到2000000,所以会最先打印main,等到子线程1执行完打印1000000,释放锁,再运行子线程2,打印2000000。

此时,执行t1.start()和t2.start()后暂停2秒,足够子线程1和子线程2执行完毕,所以先打印,最后主线程打印出num最后的值2000000。

11-使用互斥锁解决资源竞争的问题2

import threading
import time

# 定义一个全局变量
g_num = 0


def test1(num):
    global g_num

    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("-----in test1 g_num=%d----" % g_num)


def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("-----in test2 g_num=%d=----" % g_num)

# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()

def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕....
    time.sleep(2)

    print("-----in main Thread g_num = %d---" % g_num)

if __name__ == "__main__":
    main()

类来调用线程
在这里插入图片描述

死锁
在线程间共享多个资源的时候,如果两个线程分别占用一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的通知响应。

避免死锁

  1. 程序设计时要尽量避免
  2. 添加超时时间

六、案例:多任务udp聊天器

12-案例:多任务udp聊天器

import socket
import threading

def recv_msg(udp_socket):
    """接收数据并显示"""

    # 接收数据
    while True:
        recv_data = udp_socket.recvfrom(1024)
        print(recv_data)

def send_msg(udp_socket, dest_ip, dest_port):
    """发送数据"""
    # 发送数据
    while True:
        send_data = input("输入要发送的数据:")
        udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))

def main():
    """完成udp聊天器的整体控制"""

    # 1. 创建套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # 2. 绑定本地信息
    udp_socket.bind(("", 7890))

    # 3. 获取对方的ip
    dest_ip = input("请输入对方的ip:")
    dest_port = int(input("请输入对方的port:"))

    # 4. 创建2个线程,去执行相应的功能
    t_recv = threading.Thread(target=recv_msg, args=(udp_socket,))
    t_send = threading.Thread(target=send_msg, args=(udp_socket, dest_ip, dest_port))

    t_recv.start()
    t_send.start()

if __name__ == "__main__":
    main()

总结

1.多线程共享全局变量
2. 线程运行顺序不确定,所以在在多个线程在修改全局变量的时候,就会引起资源竞争,所以在程序方面注意。
3. 主线程等待子线程结束,才会结束
4. 要想主线程等待子线程运行完,在继续执行下面的代码,就需要在代码中添加t.jion().
5. 在调用Thread()时不会创建子线程,只有在调用start()方法后才会创建线程并执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值