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

本文深入探讨Python中的多线程编程,讲解多任务概念、线程间的资源共享与互斥锁的使用,以及如何避免死锁,是理解并行与并发执行的关键资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

介绍

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

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

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() —— 主线程会等待该县城结束后才会结束。
  • 并行、并发、同步、异步、互斥、阻塞是多线程必须了解的概念。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值