多线程实践

前面的一些文章和脚本都是只能做学习多线程的原理使用,实际上什么有用的事情也没有做。接下来进行多线程的实践,看一看在实际项目中是怎么使用多线程的。

图书排名示例

Bookrank.py:

该脚本通过单线程进行下载图书排名信息的调用

  1. from atexit import register
  2. from re import compile
  3. from threading import Thread
  4. from time import sleep, ctime
  5. import requests

  6. REGEX = compile('#([\d,]+) in Books')
  7. AMZN = 'https://www.amazon.com/dp/'
  8. ISBNS = {
  9. '': 'Core Python Programming',
  10. '': 'Python Web Development with Django',
  11. '': 'Python Fundamentals',
  12. }

  13. def getRanking(isbn):
  14. url = '%s%s' % (AMZN, isbn)
  15. page = requests.get(url)
  16. data = page.text
  17. return REGEX.findall(data)[0]

  18. def _showRanking(isbn):
  19. print '- %r ranked %s' % (
  20. ISBNS[isbn], getRanking(isbn))

  21. def _main():
  22. print 'At', ctime(), 'on Amazon'
  23. for isbn in ISBNS:
  24. _showRanking(isbn)

  25. @register
  26. def _atexit():
  27. print 'all DONE at:', ctime()

  28. if __name__ == '__main__':
  29. _main()
  1.  

输出结果为:

  1. /usr/bin/python ~/Test_Temporary/bookrank.py
  2. At Sat Jul 28 17:16:51 2018 on Amazon
  3. - 'Core Python Programming' ranked 322,656
  4. - 'Python Fundamentals' ranked 4,739,537
  5. - 'Python Web Development with Django' ranked 1,430,855
  6. all DONE at: Sat Jul 28 17:17:08 2018
  1.  

引入线程

上面的例子只是一个单线程程序,下面引入线程,并使用多线程再执行程序对比各自所需的时间。

​ 将上面脚本中 _main() 函数的 _showRanking(isbn) 修改以下代码:

  1. Thread(target=_showRanking, args=(isbn,)).start()

再次执行查看返回结果:

  1. /usr/bin/python ~/Test_Temporary/bookrank.py
  2. At Sat Jul 28 17:39:16 2018 on Amazon
  3. - 'Python Fundamentals' ranked 4,739,537
  4. - 'Python Web Development with Django' ranked 1,430,855
  5. - 'Core Python Programming' ranked 322,656
  6. all DONE at: Sat Jul 28 17:39:19 2018

从两个的输出结果中可以看出,使用单线程时总体完成的时间为 7s ,而使用多线程时,总体完成时间为 3s 。另外一个需要注意的是,单线程版本是按照变量的顺序输出,而多线程版本按照完成的顺序输出。

同步原语

一般在多线程代码中,总会有一些特定的函数或代码块不希望(或不应该)被多个线程同时执行,通常包括修改数据库、更新文件或其它会产生竟态条件的类似情况。这就是需要使用同步的情况。

  • 当任意数量的线程可以访问临界区的代码,但给定的时刻只有一个线程可以通过时,就是使用同步的时候了;

  • 程序员选择适合的同步原语,或者线程控制机制来执行同步;

  • 进程同步有不同的类型【参见:https://en.wikipedia.org/wiki/Synchronization_(computer_science) 】

  • 同步原语有:锁/互斥、信号量。锁是最简单、最低级的机制,而信号量用于多线程竞争有限资源的情况。

锁示例

锁有两种状态:锁定和未锁定。而且它也只支持两个函数:获得锁和释放锁。

  • 当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码;

  • 所有之后到达的线程将被阻塞,直到第一个线程结束退出临界区并释放锁;

  • 锁被释放后,其它等待的线程可以继续争夺锁,并进入临界区;

  • 被阻塞的线程没有顺序,不会先到先得,胜出的线程是不确定的。

代码示例(mtsleepF.py):

*注:该脚本派生了随机数量的线程,每个线程执行结束时会进行输出

  1. # -*- coding=utf-8 -*-
  2. from atexit import register
  3. from random import randrange
  4. from threading import Thread, currentThread
  5. from time import sleep, ctime

  6. class CleanOutputSet(set):
  7. def __str__(self):
  8. return ', '.join(x for x in self)

  9. loops = (randrange(2, 5) for x in range(randrange(3, 7)))
  10. remaining = CleanOutputSet()

  11. def loop(nsec):
  12. myname = currentThread().name
  13. remaining.add(myname)
  14. print('这个是目前线程池中的线程:', remaining)
  15. print('[%s] Started %s' % (ctime(), myname))
  16. sleep(nsec)
  17. remaining.remove(myname)
  18. print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))
  19. print(' (remaining: %s)' % (remaining or 'None'))

  20. def _main():
  21. for pause in loops:
  22. Thread(target=loop, args=(pause,)).start()

  23. @register
  24. def _atexit():
  25. print('all DONE at:%s' % ctime())

  26. if __name__ == '__main__':
  27. _main()
  1.  

执行后的输出结果:

  1. /usr/local/bin/python3.6 /Users/zhenggougou/Project/Test_Temporary/mtsleepF.py
  2. 这个是目前线程池中的线程: Thread-1
  3. [Sat Jul 28 21:09:44 2018] Started Thread-1
  4. 这个是目前线程池中的线程: Thread-2, Thread-1
  5. [Sat Jul 28 21:09:44 2018] Started Thread-2
  6. 这个是目前线程池中的线程: Thread-3, Thread-2, Thread-1
  7. [Sat Jul 28 21:09:44 2018] Started Thread-3
  8. 这个是目前线程池中的线程: Thread-3, Thread-2, Thread-4, Thread-1
  9. [Sat Jul 28 21:09:44 2018] Started Thread-4
  10. 这个是目前线程池中的线程: Thread-5, Thread-4, Thread-3, Thread-2, Thread-1
  11. [Sat Jul 28 21:09:44 2018] Started Thread-5
  12. 这个是目前线程池中的线程: Thread-5, Thread-6, Thread-4, Thread-3, Thread-2, Thread-1
  13. [Sat Jul 28 21:09:44 2018] Started Thread-6
  14. [Sat Jul 28 21:09:46 2018] Completed Thread-2 (2 secs)
  15. [Sat Jul 28 21:09:46 2018] Completed Thread-1 (2 secs)
  16. [Sat Jul 28 21:09:46 2018] Completed Thread-3 (2 secs)
  17. (remaining: Thread-5, Thread-6, Thread-4)
  18. [Sat Jul 28 21:09:46 2018] Completed Thread-6 (2 secs)
  19. (remaining: Thread-5, Thread-4)
  20. [Sat Jul 28 21:09:46 2018] Completed Thread-4 (2 secs)
  21. (remaining: Thread-5)
  22. (remaining: Thread-5)
  23. [Sat Jul 28 21:09:46 2018] Completed Thread-5 (2 secs)
  24. (remaining: None)
  25. (remaining: None)
  26. all DONE at:Sat Jul 28 21:09:46 2018

从执行结果中可以看出,有的时候可能会存在多个线程并行执行操作删除 remaining 集合中数据的情况。比如上面结果中,线程1、2、3 就是同时执行去删除集合中数据的。所以为了避免这种情况需要加锁,通过引入 Lock (或 RLock),然后创建一个锁对象来保证数据的修改每次只有一个线程能操作。

  1. 首先先导入锁类,然后创建锁对象

    from threading import Thread, Lock, currentThread

    lock = Lock()

  2. 然后使用创建的锁,将上面 mtsleepF.py 脚本中 loop() 函数做以下改变:

    1. def loop(nsec):
    2. myname = currentThread().name
    3. lock.acquire() # 获取锁
    4. remaining.add(myname)
    5. print('这个是目前线程池中的线程:', remaining)
    6. print('[%s] Started %s' % (ctime(), myname))
    7. lock.release() # 释放锁
    8. sleep(nsec)
    9. lock.acquire() # 获取锁
    10. remaining.remove(myname)
    11. print('[%s] Completed %s (%d secs)' % (ctime(), myname, nsec))
    12. print(' (remaining: %s)' % (remaining or 'None'))
    13. lock.release() # 释放锁

在操作变量的前后需要进行获取锁和释放锁的操作,以保证在修改变量时只有一个线程进行。上面的代码有两处修改变量,一是:remaining.add(myname) ,二是:remaining.remove(myname)。 所以上面代码中有两次获取锁和释放锁的操作。其实还有一种方案可以不再调用锁的 acquire()release() 方法,二是使用上下文管理,进一步简化代码。代码如下:

  1. def loop(nesc):
  2. myname = currentThread().name
  3. with lock:
  4. remaining.add(myname)
  5. print('[{0}] Started {1}'.format(ctime(), myname))
  6. sleep(nesc)
  7. with lock:
  8. remaining.remove(myname)
  9. print('[{0}] Completed {1} ({2} secs)'.format(ctime(), myname, nesc))
  10. print(' (remaining: {0})'.format(remaining or 'None'))
  1.  

信号量示例

锁非常易于理解和实现,也很容易决定何时需要它们,然而,如果情况更加复杂,可能需要一个更强大的同步原语来代替锁。

信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。可以认为信号量代表它们的资源可用或不可用。信号量比锁更加灵活,因为可以有多个线程,每个线程都拥有有限资源的一个实例。

  • 消耗资源使计数器递减的操作习惯上称为 P() —— acquire ;

  • 当一个线程对一个资源完成操作时,该资源需要返回资源池中,这个操作一般称为 V() —— release 。

示例,糖果机和信号量(candy.py):

*注:该脚本使用了锁和信号量来模拟一个糖果机

  1. # -*- coding=utf-8 -*-
  2. from atexit import register
  3. from random import randrange
  4. from threading import BoundedSemaphore, Lock, Thread
  5. from time import sleep, ctime

  6. lock = Lock()
  7. MAX = 5
  8. candytray = BoundedSemaphore(MAX)

  9. def refill():
  10. lock.acquire()
  11. print('Refilling candy')
  12. try:
  13. candytray.release() # 释放资源
  14. except ValueError:
  15. print('full, skipping')
  16. else:
  17. print('OK')
  18. lock.release()

  19. def buy():
  20. lock.acquire()
  21. print('Buying candy...')
  22. if candytray.acquire(False): # 消耗资源
  23. print('OK')
  24. else:
  25. print('empty, skipping')
  26. lock.release()

  27. def producer(loops):
  28. for i in range(loops):
  29. refill()
  30. sleep(randrange(3))

  31. def consumer(loops):
  32. for i in range(loops):
  33. buy()
  34. sleep(randrange(3))

  35. def _main():
  36. print('starting at:{0}'.format(ctime()))
  37. nloops = randrange(2, 6)
  38. print('THE CANDY MACHINE (full with %d bars)!' % MAX)
  39. Thread(target=consumer, args=(randrange(nloops, nloops+MAX+2),)).start()
  40. Thread(target=producer, args=(nloops,)).start()

  41. @register
  42. def _atexit():
  43. print('all DONE at:{0}'.format(ctime()))

  44. if __name__ == '__main__':
  45. _main()

执行结果为:

  1. /usr/local/bin/python3.6 ~/Test_Temporary/candy.py
  2. starting at:Sun Jul 29 21:12:50 2018
  3. THE CANDY MACHINE (full with 5 bars)!
  4. Buying candy...
  5. OK
  6. Refilling candy
  7. OK
  8. Refilling candy
  9. full, skipping
  10. Buying candy...
  11. OK
  12. Buying candy...
  13. OK
  14. all DONE at:Sun Jul 29 21:12:52 2018

多线程实践—Python多线程编程的更多相关文章

  1. Linux多线程实践(7) --多线程排序对比

    屏障 int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restri ...

  2. python 并发编程 多线程 目录

    线程理论 python 并发编程 多线程 开启线程的两种方式 python 并发编程 多线程与多进程的区别 python 并发编程 多线程 Thread对象的其他属性或方法 python 并发编程 多 ...

  3. 初识python多线程

    目录 GIL锁 Thread类构造方法 Lock类.Rlock类 参考: python3多线程--官方教程中文版 python多线程-1 python多线程-2.1 python多线程-2.2 pyt ...

  4. Python网络编程—socket(二)

    http://www.cnblogs.com/phennry/p/5645369.html 接着上篇博客我们继续介绍socket网络编程,今天主要介绍的内容:IO多路复用.多线程.补充知识点. 一.I ...

  5. Python - 并发编程,多进程,多线程

    传送门 https://blog.csdn.net/jackfrued/article/details/79717727 在此基础上实践和改编某些点 1. 并发编程 实现让程序同时执行多个任务也就是常 ...

  6. python多线程编程

    Python多线程编程中常用方法: 1.join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,那么在调用线程的时就可以使用被调线程的join方法join( ...

  7. 关于python多线程编程中join()和setDaemon()的一点儿探究

    关于python多线程编程中join()和setDaemon()的用法,这两天我看网上的资料看得头晕脑涨也没看懂,干脆就做一个实验来看看吧. 首先是编写实验的基础代码,创建一个名为MyThread的  ...

  8. 深入 HTML5 Web Worker 应用实践:多线程编程

    深入 HTML5 Web Worker 应用实践:多线程编程 HTML5 中工作线程(Web Worker)简介 至 2008 年 W3C 制定出第一个 HTML5 草案开始,HTML5 承载了越来越 ...

  9. day-3 python多线程编程知识点汇总

    python语言以容易入门,适合应用开发,编程简洁,第三方库多等等诸多优点,并吸引广大编程爱好者.但是也存在一个被熟知的性能瓶颈:python解释器引入GIL锁以后,多CPU场景下,也不再是并行方式运 ...

随机推荐

  1. [Laravel框架学习一]:Laravel框架的安装以及 Composer的安装

    1.先下载Composer-Setup.exe,下载地址:下载Composer .会自动搜索PHP.exe的安装路径,如果没有,就手动找到php路径下的php.exe. 2.在PHP目录下,打开php ...

  2. 一站式轻量级框架 Spring

    Spring 简介 Spring 是一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的.Spring 的核心是控制反转(IoC)和面向切面编程(AOP).简单来说,Spring ...

  3. Shelve:对象的持久化存储

    目的:Shelve模块为任意能够pickle的Python对象实现持久化存储,并提供一个类似字典的接口. 在关系型数据库还过于复杂的情境中,Shelve为你提供了Python对象持久化的另一种方案. ...

  4. MySQL为某字段加前缀、后缀

    在开发过程中,可能会遇到加前缀或者后缀的情况.比如为视频添加路径时,如果手动加起来肯定慢,而且比较不符合程序员的特点,我们就应该能让程序跑就不会手动加. 使用UPDATE sql 语句:update ...

  5. sudo: 在加载插件“sudoers_policy”时在 /etc/sudo.conf 第 0 行出错 sudo: /usr/lib/sudo/sudoers.so 必须只对其所有者可写 sudo: 致命错误,无法加载插件

    解决办法:  su root chmod 644 /usr/lib/sudo/sudoers.so chown -R root /usr/lib/sudo 千万不要给 /usr 赋全部权限!!!   ...

  6. thinkphp5.1生成缩略图很模糊

    缩略图一定要从大分辨率往小生成 $image->thumb(400,400,\think\Image::THUMB_CENTER)->save(Env::get('root_path'). ...

  7. 作业九——DFA最小化,语法分析初步

  8. Configure Visual Studio with UNIX end of lines

    As OP states "File > Advanced Save Options", select Unix Line Endings. https://stackove ...

  9. Node 接入阿里云实现短信验证码

    本文介绍在案例云开通短信服务的流程以及在Node项目中使用的方法. 一.开通阿里云短信服务 登陆阿里云,然后进入 https://dysms.console.aliyun.com/dysms.htm  ...

  10. vue2.x学习笔记(二十七)

    接着前面的内容:https://www.cnblogs.com/yanggb/p/12682364.html. 单元测试 vue cli拥有开箱即用的通过jest或mocha进行单元测试的内置选项.官 ...