【python】多线程小结

every blog every motto: Light tomorrow with today.

0. 前言

前不久有一个任务用到了多线程,也简单的学习了下,但是没有做相关记录,今天有空再复习一遍,并简单记录下来。
说明: 后续可能会增补

1. 正文

1.1 基本概念

1>. 单线程&多线程

现在我们有两个任务:烧水和拖地。
单线程: 先烧水,等水烧开,再进行拖地。等待烧水的过程的时间(资源)就白白浪费了。
多线程: 先烧水,烧水的过程(相当于IO操作)把时间(资源)让出来给拖地,地拖完了,水也烧开了。
以上就多线程的优势的直观理解

2>. 线程和进程
  • 一个任务就是一个进程,比如打开浏览器、启动微信等
  • 有些进程同时能做多件事,比如,word可以同时进行打字、拼写检查、打印等,这里多件事,我们称为多个“子任务”,我们把这些“子任务“称为线程
  • 一个程序至少一个进程,一个进程至少一个线程。
  • 多线程可以共享全局变量
3>. 并行&并发

并行(parallel): 是计算机系统中能够同时执行两个或更多个处理的一种计算方法,可以同时工作于同一程序的不同方面,并行处理的主要目的是节省大型和复杂问题的解决时间。
在这里插入图片描述
并发(concurrent):同一时间段有几个程序都处于已启动运行和运行完毕之间。且这几个程序都是在同一个处理器(CPU)上运行,但任意时刻上只有一个程序在CPU上运行
在这里插入图片描述
并发是指一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序运行
小结:

  • 区别:
  1. 并发,多个事情,在同一时间段内同时发生。
  2. 并行,多个事情,在同一时间点上同时发生。
  3. 并发的多个任务之间时互相抢占资源的。
  4. 并行的多个任务之间时不互相抢占资源的
  5. 只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
    在这里插入图片描述
4>. 同步&异步

同步异步:
同步: 指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
异步: 指进程不需要一直等待下去,而实继续执行下面的操作,不管其他进程的状态,当有消息返回时系统会通知进程进行处理,这样可以提高执行效率

5>. GIL(全局解释锁,Global Interpreter Lock)

GIL并不是python的特性,他是实现python解释器(C时引入的),
作用: 限制多线程同时执行,保证同一时刻只有一个线程执行
原因: 线程并非独立,在一个进程中多个线程共享变量,多个线程执行会导致数据被污染造成数据混乱,这个就是线程不安全性,为此引入互斥锁。
互斥锁: 即确保某段关键代码的数据只能有一个线程从头到尾完整运行,保证了这段代码数据的安全性,但这就导致了死锁。
死锁: 多个子线程在等待对方解除占用状态,但是都不先解锁,互相带灯,这就是死锁。

  1. GIL说白了就是为多线程,一个线程运行其他线程阻塞,使你的多线程代码不是同时执行,而实交替执行。
    在这里插入图片描述
  2. 基于GIL的存在,在遇到大量IO操作(文件读写、网络等待)时,使用多线程效率更高。

1.2 多线程部分

用装饰器查看函数运行时间,有关装饰器介绍,点我

import time
import functools

c = 0 # 全局变量

def count_time(func):
    """装饰器:计算程序运行时间"""

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        t1 = time.time()
        res = func(*args, **kwargs)
        t2 = time.time()

        # 计算时间
        time_consumed = t2 - t1
        print('{}函数一共花费了{}秒'.format(func.__name__, time_consumed))
        return res

    return wrapper

1.2.1 测试主体为两个函数

两个被测试的函数:

@count_time
def mop_floor(arg):
    """拖地操作,"""
    global c
    print('开始拖地……')
    for i in range(3):
        time.sleep(1)
        c += 5
        c -= 5
        print('{} is running……c={} !!'.format(arg, c))
    print('---------地拖完了!-----------')


@count_time
def heat_water(arg):
    """烧水操作"""
    global c
    print('我要烧水了……')
    for i in range(4):
        time.sleep(1)
        print('{} is running……c={} !!'.format(arg, c))
    print('------------水烧开了!-------------')

>1. 单线程

单线程主程序:

@count_time
def single_thread():
    """单线程程序"""
    mop_floor('1')
    heat_water('2')


def main():
    # 单线程
    single_thread()
    # my_processing()


if __name__ == '__main__':
    main()

运行结果:
可以看到两个函数依次执行,程序总耗时 7秒
在这里插入图片描述

>2. 多线程
@count_time
def my_thread():
    # mop_floor('1')
    # heat_water('2')
    t1 = threading.Thread(target=mop_floor, args=('1',))
    t2 = threading.Thread(target=heat_water, args=('2',))
    # t1.setDaemon(True)
    # t2.setDaemon(True)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('-' * 100)

def main():
    # 单线程
    # single_thread()

    # 多线程
    my_thread()



if __name__ == '__main__':
    main()

运行结果:
程序总耗时4秒,可以发现,总的运行时间减少了
在这里插入图片描述
说明:
join: join方法让主线程阻塞,等待其创建的线程执行完成

3>. 尝试无join

对多线程主体程序进行修改

@count_time
def my_thread():
    # mop_floor('1')
    # heat_water('2')
    t1 = threading.Thread(target=mop_floor, args=('1',))
    t2 = threading.Thread(target=heat_water, args=('2',))

    t1.start()
    t2.start()

    print('-' * 100)

结果:
不加join ,当主线程执行完毕之后,当前程序并不会结束,必须等到所有线程都结束之后才能结束当前进程。
在这里插入图片描述

4>. 尝试守护线程(daemon)

可以通过将创建的线程指定为守护线程(daemon) ,这样主线程执行完毕之后会立即结束当前为执行的线程,然后结束程序。
守护线程作用: 如果当前python线程是守护线程,那么意味着这个线程是 “不重要” 的,”不重要“意味着如果他的主线程结束了但该守护线程没有运行完,守护线程会被强制结束。

@count_time
def my_thread():
    # mop_floor('1')
    # heat_water('2')
    t1 = threading.Thread(target=mop_floor, args=('1',))
    t2 = threading.Thread(target=heat_water, args=('2',))
    t1.setDaemon(True)
    t2.setDaemon(True)
    t1.start()
    t2.start()
    # t1.join()
    # t2.join()
    print('-' * 100)

主线程执行完毕以后会立即结束未执行的线程。
在这里插入图片描述

1.2.2 测试主体为一个计算型函数

被测试函数为一个计算型函数

@count_time
def single_func_tested(arg):
    """对一个函数进行测试"""
    sum = 0
    for i in range(10000000):
        sum += i
    print('{} over'.format(arg))

>1. 单线程

单线程

@count_time
def single_thread():
    """单线程程序"""

    # 对一个函数进行测试
    single_func_tested('1')
	print('-'*100)

主函数

def main():
    # 单线程
    single_thread()


if __name__ == '__main__':
    main()

结果
在这里插入图片描述

>2. 多线程

多线程程序

@count_time
def my_thread():
  
    thread_array = {}
    n = 1
    for tid in range(n):
        t = threading.Thread(target=single_func_tested, args=('1',))
        t.start()
        thread_array[tid] = t
    for i in range(n):
        thread_array[i].join()

    print('-' * 100)

def main():
    # 单线程
    # single_thread()

    # 多线程
    my_thread()
    # my_processing()


if __name__ == '__main__':
    main()

测试与结果:
当n=1时:
在这里插入图片描述
当 n=2 时,
在这里插入图片描述
当n=3时,
在这里插入图片描述
对单个函数开多线程小结:
由以上可以发现,随着线程数的增加,总运行时间并没有减少反而增加了,同时,由打印结果可以发现,对单个函数开多线程仅是对单个函数重复执行几遍而已

1.2.2 测试主体为一个等待型函数

被测试函数为一个I/O型函数

@count_time
def mop_floor(arg):
    """拖地操作,"""
    global c
    print('开始拖地……')
    for i in range(3):
        time.sleep(1)
        c += 5
        c -= 5
        print('{} is running……c={} !!'.format(arg, c))
    print('---------地拖完了!-----------')

>1. 单线程

单线程

@count_time
def single_thread():
    """单线程程序"""


    mop_floor('1')
    print('-'*100)

主函数

def main():
    # 单线程
    single_thread()

    # 多线程
    # my_thread()
    # my_processing()


if __name__ == '__main__':
    main()

在这里插入图片描述

>2. 多线程

多线程函数

@count_time
def my_thread():

    thread_array = {}
    n = 3
    for tid in range(n):
        t = threading.Thread(target=mop_floor, args=('1',))
        t.start()
        thread_array[tid] = t
    for i in range(n):
        thread_array[i].join()

    print('-' * 100)

主函数:

def main():

    # 多线程
    my_thread()


if __name__ == '__main__':
    main()

在这里插入图片描述
由以上可以发现,当n=3时,运行时间和单线相同(其中,n=1,2结果亦是相同,读者可自行i验证),和上面的单个计算型函数一样,对单个等待型函数也是将其重复执行罢了,但不同的时,单个等待型函数总运行时间和单线程运行时间相同,并未随n的增加而增加。
进一步总结:

  1. 对单个函数开多线程无意义,仅将其重复多遍而已
  2. 个人猜测:
  • 单个计算型函数 对应 CPU密集型
  • 单个等待型函数 对应 I/O密集型
  1. 这篇文章对单个函数测试开多线程,并不能说明问题。

1.2.2 测试主体为一个函数(改进版)

版本一:

@count_time
def single_func_tested(arg):
    """对一个函数进行测试"""
    sum = 0
    for i in range(100):  # 100000
        sum += i
        time.sleep(0.1)
    for i in range(100):
        sum += i
    print('{} over'.format(arg))

版本二:

@count_time
def single_func_tested(arg):
    """对一个函数进行测试"""
    sum = 0
    for i in range(100):  # 100000
        sum += i
        
    time.sleep(0.1)
    
    for i in range(100):
        sum += i
    print('{} over'.format(arg))

注:

  1. 以上版本,不贴具体结果,由兴趣的读者,可自行尝试。经实验后发现,使用多线程均不能缩短程序运行时间。
  2. 得出对单个程序使用多线程无效(此观点可锤,欢迎)

1.3 源码

https://gist.github.com/onceone/f5489dcc1499c81ee0161f0ea3bb442b

参考文献

[1] https://blog.csdn.net/weixin_39190382/article/details/107107980
[2] http://www.uml.org.cn/python/201901221.asp
[3] https://www.jianshu.com/p/644dbb6d4cc8
[4] https://blog.csdn.net/m0_37324740/article/details/85765167
[5] https://www.cnblogs.com/-qing-/p/11291581.html
[6] https://blog.csdn.net/lzy98/article/details/88819425
[7] https://www.cnblogs.com/yssjun/p/11302500.html
[8] https://www.jb51.net/article/167165.htm
[9] https://blog.csdn.net/weixin_44850984/article/details/89165731
[10] https://www.cnblogs.com/justbreaking/p/7218909.html?utm_source=itdadao&utm_medium=referral

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡侃有料

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值