本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是Python专题的第21篇文章,我们继续多线程的话题。

上周的文章当中我们简单介绍了线程和进程的概念,以及在Python当中如何在主线程之外创建其他线程,并且还了解了用户级线程和后台线程的区别以及使用方法。今天我们来看看线程的其他使用,比如如何停止一个线程,线程之间的Event用法等等。

停止线程

利用Threading库我们可以很方便地创建线程,让它按照我们的想法执行我们想让它执行的事情,从而加快程序运行的效率。然而有一点坑爹的是,线程创建之后,就交给了操作系统执行,我们无法直接结束一个线程,也无法给它发送信号,无法调整它的调度,也没有其他高级操作。如果想要相关的功能,只能自己开发。

怎么开发呢?

我们创建线程的时候指定了target等于一个我们想让它执行的函数,这个函数并不一定是全局函数,实际上也可以是一个对象中的函数。如果是对象中的函数,那么我们就可以在这个函数当中获取到对象中的其他信息,我们可以利用这一点来实现手动控制线程的停止。

说起来好像不太好理解,但是看下代码真的非常简单:

import time
from threading import Thread class TaskWithSwitch:
def __init__(self):
self._running = True def terminate(self):
self._running = False def run(self, n):
while self._running and n > 0:
print('Running {}'.format(n))
n -= 1
time.sleep(1) c = TaskWithSwitch()
t = Thread(target=c.run, args=(10, ))
t.start()
c.terminate()
t.join()

如果你运行这段代码,会发现屏幕上只输出了10,因为我们将_running这个字段置为False之后,下次循环的时候不再满足循环条件,它就会自己退出了。

如果我们想要用多线程来读取IO,由于IO可能存在堵塞,所以可能会出现线程一直无法返回的情况。也就是说我们在循环内部卡死了,这个时候单纯用_running来判断还是不够的,我们需要在线程内部设置计时器,防止循环内部的卡死。

class IOTask:
def __init__(self):
self._running = True def terminate(self):
self._running = False def run(self, sock):
# 在socket中设置计时器
sock.settimeout(10)
while self._running:
try:
# 由于设置了计时器,所以这里不会永久等待
data = sock.recv(1024)
break
except socket.timeout:
continue
return

线程信号的传递

我们之所以如此费劲才能控制线程的运行,主要原因是线程的状态是不可知的,并且我们无法直接操作它,因为它是被操作系统管理的。我们运行的主线程和创建出来的线程是独立的,两者之间并没有从属关系,所以想要实现对线程的状态进行控制,往往需要我们通过其他手段来实现。

我们来思考一个场景,假设我们有一个任务,需要在另外一个线程运行结束之后才能开始执行。要想要实现这一点,就必须对线程的状态有所感知,需要其他线程传递出信号来才行。我们可以使用threading中的Event工具来实现这一点。Event工具就是可以用来传递信号的,就好像是一个开关,当一个线程执行完成之后,会去启动这个开关。而这个开关控制着另外一段逻辑的运行。

我们来看下样例代码:

import time
from threading import Thread, Event def run_in_thread():
time.sleep(1)
print('Thread is running') t = Thread(target=run_in_thread)
t.start() print('Main thread print')

我们在线程里面就只做了输出一行提示符,没有其他任何逻辑。由于我们在run_in_thread函数当中沉睡了1s,所以一定是先输出Main thread print再输出的Thread is running。假设这个线程是一个很重要的任务,我们希望主线程能够等待它运行到一个阶段再往下执行,我们应该怎么办呢?

注意,这里说的是运行到一个阶段,并不是运行结束。运行结束我们很好处理,可以通过join来完成。但如果不是运行结束,而是运行完成了某一个阶段,当然通过join也可以,但是会损害整体的效率。这个时候我们就必须要用上Event了。加上Event之后,我们再来看下代码:

import time
from threading import Thread, Event def run_in_thread(event):
time.sleep(1)
print('Thread is running')
# set一下event,这样外面wait的部分就会被启动
event.set() # 初始化Event
event = Event()
t = Thread(target=run_in_thread, args=(event, ))
t.start() # event等待set
event.wait()
print('Main thread print')

整体的逻辑没有太多的修改,主要的是增加了几行关于Event的使用代码。

我们如果要用到Event,最好在代码当中只使用一次。当然通过Event中的clear方法我们可以重置Event的值,但问题是我们没办法保证重置的这个逻辑会在wait之前执行。如果是在之后执行的,那么就会问题,并且在debug的时候会异常痛苦,因为bug不是必现的,而是有时候会出现有时候不会出现。这种情况往往都是因为多线程的使用问题。

所以如果要多次使用开关和信号的话,不要使用Event,可以使用信号量。

信号量

Event的问题在于如果多个线程在等待Event的发生,当它一旦被set的时候,那么这些线程都会同时执行。但有时候我们并不希望这样,我们希望可以控制这些线程一个一个地运行。如果想要做到这一点,Event就无法满足了,而需要使用信号量。

信号量和Event的使用方法类似,不同的是,信号量可以保证每次只会启动一个线程。因为这两者的底层逻辑不太一致,对于Event来说,它更像是一个开关。一旦开关启动,所有和这个开关关联的逻辑都会同时执行。而信号量则像是许可证,只有拿到许可证的线程才能执行工作,并且许可证一次只发一张。

想要使用信号量并不需要自己开发,thread库当中为我们提供了现成的工具——Semaphore,我们来看它的使用代码:

# 工作线程
def worker(n, sema):
# 等待信号量
sema.acquire()
print('Working', n) # 初始化
sema = threading.Semaphore(0)
nworkers = 10
for n in range(nworkers):
t = threading.Thread(target=worker, args=(n, sema,))
t.start()

在上面的代码当中我们创建了10个线程,虽然这些线程都被启动了,但是都不会执行逻辑,因为sema.acquire是一个阻塞方法,没有监听到信号量是会一直挂起等待。

当我们释放信号量之后,线程被启动,才开始了执行。我们每释放一个信号,则会多启动一个线程。这里面的逻辑应该不难理解。

总结

在并发场景当中,多线程的使用绝不是多启动几个线程做不同的任务而已,我们需要线程间协作,需要同步、获取它们的状态,这是非常不容易的。一不小心就会出现幽灵bug,时显时隐,这也是并发问题让人头疼的主要原因。

这篇文章当中我们只是简单介绍了线程间通信的基本方法,针对这个问题,还有更好的解决方案。我们将在后续的文章当中继续讨论这个问题,敬请期待。

今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。

本文使用 mdnice 排版

Python | Python初学者的自我修养,找到自己的方向的更多相关文章

  1. Python初学者的自我修养,找到自己的方向

    今天是 Python专题 的第22篇文章,原本今天是准备和大家继续Python当中多线程的使用的相关内容.然而前两天有一个读者在后台问我,学习Python有哪些适合新手入门的小项目推荐,所以今天这篇临 ...

  2. Python学习笔记(四十九)爬虫的自我修养(一)

    论一只爬虫的自我修养 URL的一般格式(带括号[]的为可选项): protocol://hostname[:port]/path/[;parameters][?query]#fragment URL由 ...

  3. Python编程初学者指南|百度网盘免费下载|Python新手入门资料

    Python编程初学者指南|百度网盘免费下载 提取码:9ozx 目录  · · · · · · 第1章 启程:Game Over程序1.1 剖析Game Over程序1.2 Python简介1.2.1 ...

  4. 为什么Python适合初学者,一般要学习多久

    为什么Python适合初学者?一般Python要学习多久?很多人都觉得,Python是一门很好学的语言,非常适合入门.但更多人都是不清楚具体原因的.那么,我们不如一起来看看Python为何更适合初学者 ...

  5. 《Python编程初学者指南》高清PDF版|百度网盘免费下载|Python基础

    <Python编程初学者指南>|百度网盘免费下载| 提取码:03b1 内容简介 Python是一种解释型.面向对象.动态数据类型的高级程序设计语言.Python可以用于很多的领域,从科学计 ...

  6. Python编程初学者指南PDF高清电子书免费下载|百度云盘

    百度云盘:Python编程初学者指南PDF高清电子书免费下载 提取码:bftd 内容简介 Python是一种解释型.面向对象.动态数据类型的高级程序设计语言.Python可以用于很多的领域,从科学计算 ...

  7. 《web全栈工程师的自我修养》读书笔记

    有幸读了yuguo<web全栈工程师的自我修养>,颇有收获,故在此对读到的内容加以整理,方便指导,同时再回顾一遍书中的内容. 概览 整本书叙述的是作者的成长经历,通过经验的分享,给新人或者 ...

  8. python --- Python中的callable 函数

    python --- Python中的callable 函数 转自: http://archive.cnblogs.com/a/1798319/ Python中的callable 函数 callabl ...

  9. 程序员的自我修养(2)——计算机网络(转) good

    相关文章:程序员的自我修养——操作系统篇 几乎所有的计算机程序,都会牵涉到网络通信.因此,了解计算机基础网络知识,对每一个程序员来说都是异常重要的. 本文在介绍一些基础网络知识的同时,给出了一些高质量 ...

随机推荐

  1. jmeter对数据库进行简单的压测

    1.点击测试计划,再点击“浏览”,把JDBC驱动添加进来: 注:JDBC驱动一般的位置在java的安装地址下,路径类似于:    \java\jre\lib\ext 文件为:mysql-connect ...

  2. cc40a_demo_Cpp_智能指针c++_txwtech

    //40_21days_Cpp_智能指针c++_cc40a_demo.cpp_txwtech //智能指针.auto_ptr类//*常规指针-容易产生内存泄漏,内存被占满,程序就死机,或者系统死机// ...

  3. WeChair项目Alpha冲刺(4/10)

    团队项目进行情况 1.昨日进展    Alpha冲刺第四天 昨日进展: 前端完成小程序登录态的定义 LoginController编写初步完成同时修改并更新了代码,但是在将编码好的项目部署到服务器上时 ...

  4. TCP实战一(三握四挥、流量控制)

    上一篇博文已经介绍了tcpdump的一些基本操作与命令,今天这篇博文将带你解密如何利用wireshark对tcpdump抓到的数据包进行可视化分析! 参考文献:https://zhuanlan.zhi ...

  5. Spring AOP学习笔记04:AOP核心实现之创建代理

    上文中,我们分析了对所有增强器的获取以及获取匹配的增强器,在本文中我们就来分析一下Spring AOP中另一部分核心逻辑--代理的创建.这部分逻辑的入口是在wrapIfNecessary()方法中紧接 ...

  6. @PathVariable @RequestParam@RequestBody

    @PathVariable 当使用@RequestMapping URI template 样式映射时, 即 someUrl/{paramId}, 这时的paramId可通过 @Pathvariabl ...

  7. MongoDB副本集replica set (二)--副本集环境搭建

    (一)主机信息 操作系统版本:centos7 64-bit 数据库版本   :MongoDB 4.2 社区版 ip hostname 192.168.10.41 mongoserver1 192.16 ...

  8. 第十章:RDB持久化

    RDB 保存命令 save 命令,阻塞 Redis 服务器进程,直到保存动作完成: bgsave 命令,派生出一个子进程来完成保存动作: 载入命令 Redis 没有载入 RDB 文件的命令,载入动作在 ...

  9. 【原创】Linux中断子系统(四)-Workqueue

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  10. CentOS 7 安装VNC

    VNC需要系统安装的有桌面,可以进行下面操作安装GNOME 桌面. #列出的组列表里有GNOME Desktopyum grouplist | grep GNOME 安装 yum groupinsta ...