快速了解Python并发编程的工程实现(上)
关于我
一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
Github:https://github.com/hylinux1024
微信公众号:终身开发者(angrycode)
0x00 前言
在前面的文章中对Python
协程的概念和实现做了简单地介绍。为了对Python
并发编程有更加全面地认识,我也对Python
线程和进程的概念和相关技术的使用进行了学习,于是有了这篇文字。
0x01 线程与进程
当我们在手机或者PC
上打开一个应用时,操作系统就会创建一个进程实例,并开始执行进程里的主线程,它有独立的内存空间和数据结构。线程是轻量级的进程。在同一个进程里,多个线程共享内存和数据结构,线程间的通信也更加容易。
0x02 使用线程实现并发
熟悉Java
编程的同学就会发现Python
中的线程模型与Java
非常类似。下文我们将主要使用Python
中的线程模块threading
包。(对于低级别的API
模块thread
不推荐初学者使用。本文所有代码将使用Python 3.7
的环境)
threading
要使用线程我们要导入threading
包,这个包是在_thread
包(即上文提到的低级别thread
模块)的基础上封装了许多高级的API
,在开发中应当首选threading
包。
常见地,有两种方式构建一个线程:通过Thread
的构造函数传递一个callable
对象,或继承Thread
类并重写run
方法。
import threading
import time
def do_in_thread(arg):
print('do in thread {}'.format(arg))
time.sleep(2)
if __name__ == '__main__':
start_time = time.time()
t1 = threading.Thread(target=do_in_thread, args=(1,), name='t1')
t2 = threading.Thread(target=do_in_thread, args=(2,), name='t2')
t1.start()
t2.start()
# join方法让主线程等待子线程执行完毕
t1.join()
t2.join()
print("\nduration {} ".format(time.time() - start_time))
# do in thread 1
# do in thread 2
# duration 2.001628875732422
还可以通过继承threading.Thread
类定义线程
import threading
import time
def do_in_thread(arg):
print('do in thread {}'.format(arg))
time.sleep(2)
class MyThread(threading.Thread):
def __init__(self, arg):
super().__init__()
self.arg = arg
def run(self):
start_time = time.time()
do_in_thread(self.arg)
print("duration {} ".format(time.time() - start_time))
def start_thread_2():
start_time = time.time()
print("duration {} ".format(time.time() - start_time))
if __name__ == '__main__':
mt1 = MyThread(3)
mt2 = MyThread(4)
mt1.start()
mt2.start()
# join方法让主线程等待子线程执行完毕
mt1.join()
mt2.join()
# do in thread 3
# do in thread 4
# duration 2.004937171936035
join
方法的作用是让调用它的线程等待其执行完毕。
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
定义线程时可以通过指定构造方法的name
参数设置线程名称。
target
用于指定callable
对象,将在run
方法中被调用。
args
设置target
对象被调用时的参数,类型是元组()
,例如上文中的do_in_thread(arg)
方法的参数。
kwargs
是一个字典类型的参数,也用于target
对象的参数。
daemon
设置守护线程的标识,如果设置为True
那么这个线程就是守护线程,此时如果主线程结束了,那么守护线程也会立即被杀死。所以当有在守护线程中打开文件、数据库等资源操作时,释放资源就有可能出错。
线程池
程序中若有大量的线程创建和销毁,则对性能影响较大。我们可以使用线程池。同样地,它的API
与Java
极为相似。
Executor
concurrent.futures.Executor
这是一个抽象类,定义了线程池的接口。
submit(fn, *args, **kwargs)
执行fn(args,kwargs) 并会返回一个future
对象,通过future
可获取到执行结果map(func, *iterables, timeout=None, chunksize=1)
这个方法与map(func,*iterables)
类似shutdown(wait=True)
关闭线程池
from concurrent.futures import ThreadPoolExecutor
# 使用max_workers参数指定线程池中线程的最大数量为2
with ThreadPoolExecutor(max_workers=2) as executor:
# 提交任务到线程池
future = executor.submit(pow, 2, 31) # 计算2^31
future2 = executor.submit(pow, 1024, 2)
# 使用future 获取执行结果
print(future.result())
print(future2.result())
# 执行结果
# 2147483648
# 1048576
同步
若有多个线程对同一个资源或内存进行访问或操作就有会产生竞争条件。
Python
提供了锁、信号量、条件和事件等同步原语可帮忙我们实现线程的同步机制。
Lock
Lock
有两种状态:locked
和unlocked
。它有两个基本方法:acquire()
和release()
,且都是原子操作的。
一个线程通过acquire()
获取到锁,Lock
的状态变成locked
,而其它线程调用acquire()
时只能等待锁被释放。 当线程调用了release()
时Lock
的状态就变成了unlocked
,此时其它等待线程中只有一个线程将获得锁。
import threading
share_mem_lock = 0
share_mem = 0
count = 1000000
locker = threading.Lock()
def add_in_thread_with_lock():
global share_mem_lock
for i in range(count):
locker.acquire()
share_mem_lock += 1
locker.release()
def minus_in_thread_with_lock():
global share_mem_lock
for i in range(count):
locker.acquire()
share_mem_lock -= 1
locker.release()
def add_in_thread():
global share_mem
for i in range(count):
share_mem += 1
def minus_in_thread():
global share_mem
for i in range(count):
share_mem -= 1
if __name__ == '__main__':
t1 = threading.Thread(target=add_in_thread_with_lock)
t2 = threading.Thread(target=minus_in_thread_with_lock)
t3 = threading.Thread(target=add_in_thread)
t4 = threading.Thread(target=minus_in_thread)
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()
print("share_mem_lock : ", share_mem_lock)
print("share_mem : ", share_mem)
# 执行结果
# share_mem_lock : 0
# share_mem : 51306
没有使用锁机制的代码执行后最后的值很有可能就不为0。而有锁的代码则可以保证同步。
RLock
RLock
即Reentrant Lock
,就是可以重复进入的锁,也叫递归锁。它有3个特点:
- 谁获取锁谁释放。如A线程获取了锁,那么只有A线程才能释放该锁
- 同一线程可重复多次获取该锁。即可以调用
acquire
多次 acquire
多少次,对应release
就多少次,且最后一次release
才会释放锁。
Condition
条件是另一种同步原语机制。其实它的内部是封装了RLock
,它的acquire()
和release()
方法就是RLock
的方法。
Condition
常用的API
还有wait()
、notify()
和notify_all()
方法。 wait()
方法会释放锁,然后进入阻塞状态直到其它线程通过notify()
或notify_all()
唤醒自己。wait()
方法重新获取到锁就会返回。
notify()
会唤醒其中一个等待的线程,而notify_all()
会唤醒所有等待的线程。
需要注意的是notify()
或notify_all()
执行后并不会释放锁,只有调用了release()
方法后锁才会释放。
让我们看一个来自于《Python并行编程手册》中的一个生产者与消费者例子
from threading import Thread, Condition
import time
items = []
condition = Condition()
class consumer(Thread):
def __init__(self):
Thread.__init__(self)
def consume(self):
global condition
global items
# 获取锁
condition.acquire()
if len(items) == 0:
# 当items为空时,释放了锁,并等待生产者notify
condition.wait()
print("Consumer notify : no item to consume")
# 开始消费
items.pop()
print("Consumer notify : consumed 1 item")
print("Consumer notify : items to consume are " + str(len(items)))
# 消费之后notify唤醒生产者,因为notify不会释放锁,所以还要调用release释放锁
condition.notify()
condition.release()
def run(self):
for i in range(0, 10):
time.sleep(2)
self.consume()
class producer(Thread):
def __init__(self):
Thread.__init__(self)
def produce(self):
global condition
global items
condition.acquire()
if len(items) == 5:
# 若items时满的,则执行wait,释放锁,并等待消费者notify
condition.wait()
print("Producer notify : items producted are " + str(len(items)))
print("Producer notify : stop the production!!")
# 开始生产
items.append(1)
print("Producer notify : total items producted " + str(len(items)))
# 生产后notify消费者,因为notify不会释放锁,所以还执行了release释放锁。
condition.notify()
condition.release()
def run(self):
for i in range(0, 10):
time.sleep(1)
self.produce()
if __name__ == "__main__":
producer = producer()
consumer = consumer()
producer.start()
consumer.start()
producer.join()
consumer.join()
Semaphore
信号量内部维护一个计数器。acquire()
会减少这个计数,release()
会增加这个计数,这个计数器永远不会小于0。当计数器等于0时,acquire()
方法就会等待其它线程调用release()
。
还是借助一个生产者与消费者的例子来理解
# -*- coding: utf-8 -*-
"""Using a Semaphore to synchronize threads"""
import threading
import time
import random
# 默认情况内部计数为1,这里设置为0。
# 若设置为负数则会抛出ValueError
semaphore = threading.Semaphore(0)
def consumer():
print("consumer is waiting.")
# 获取一个信号量,因为初始化时内部计数设置为0,所以这里一开始时是处于等待状态
semaphore.acquire()
# 开始消费
print("Consumer notify : consumed item number %s " % item)
def producer():
global item
time.sleep(2)
# create a random item
item = random.randint(0, 1000)
# 开始生产
print("producer notify : produced item number %s" % item)
# 释放信号量, 内部计数器+1。当有等待的线程发现计数器大于0时,就会唤醒并从acquire方法中返回
semaphore.release()
if __name__ == '__main__':
for i in range(0, 5):
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
print("program terminated")
信号量经常会用于资源容量确定的场景,比如数据库连接池等。
Event
事件在线程间的通信方式非常简单。一个线程发送事件另一个线程等待接收。
Event
对象内部维护了一个bool
变量flag
。通过set()
方法设置该变量为True
,clear()
方法设置flag
为False
。wait()
方法会一直等待直到flag
变成True
结合例子
# -*- coding: utf-8 -*-
import time
from threading import Thread, Event
import random
items = []
event = Event()
class consumer(Thread):
def __init__(self, items, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
while True:
time.sleep(2)
# 等待事件
self.event.wait()
# 开始消费
item = self.items.pop()
print('Consumer notify : %d popped from list by %s' % (item, self.name))
class producer(Thread):
def __init__(self, integers, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
global item
while True:
time.sleep(2)
# 开始生产
item = random.randint(0, 256)
self.items.append(item)
print('Producer notify : item N° %d appended to list by %s' % (item, self.name))
print('Producer notify : event set by %s' % self.name)
# 发送事件通知消费者消费
self.event.set()
print('Produce notify : event cleared by %s ' % self.name)
# 设置事件内部变量为False,随后消费者线程调用wait()方法时,进入阻塞状态
self.event.clear()
if __name__ == '__main__':
t1 = producer(items, event)
t2 = consumer(items, event)
t1.start()
t2.start()
t1.join()
t2.join()
Timer
定时器Timer
是Thread
的子类。用于处理定时执行的任务。启动定时器使用start()
,取消定时器使用cancel()
。
from threading import Timer
def hello():
print("hello, world")
t = Timer(3.0, hello)
t.start() # 3秒后 打印 "hello, world"
with语法
Lock
、RLock
、Condition
和Semaphore
可以使用with
语法。
这几个对象都实现拥有acquire()
和release()
方法,且都实现了上下文管理协议。
with some_lock:
# do something...
等价于
some_lock.acquire()
try:
# do something...
finally:
some_lock.release()
0x03 小结
本文主要介绍Python
中线程的使用,主要是对threading
模块中Thread
对象、线程池Executor
常见用法的展示。还了解了线程的同步原语Lock
、RLock
、Condition
、Semaphore
、Event
以及Timer
等API
的使用。
0x04 引用
- https://python-parallel-programmning-cookbook.readthedocs.io
- https://docs.python.org/3/library/threading.html
- https://docs.python.org/3.7/library/multiprocessing.html
- https://docs.python.org/3/glossary.html#term-global-interpreter-lock
- https://docs.python.org/3/library/concurrent.futures.html#module-concurrent.futures
快速了解Python并发编程的工程实现(上)的更多相关文章
- 快速了解Python并发编程的工程实现(下)
关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...
- Python并发编程二(多线程、协程、IO模型)
1.python并发编程之多线程(理论) 1.1线程概念 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程(流水线的工作需要电源,电源就相当于 ...
- Python并发编程一(多进程)
1.背景知识(进程.多道技术) 顾名思义,进程即正在执行的一个过程.进程是对正在运行程序的一个抽象. 进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一 ...
- python并发编程之进程、线程、协程的调度原理(六)
进程.线程和协程的调度和运行原理总结. 系列文章 python并发编程之threading线程(一) python并发编程之multiprocessing进程(二) python并发编程之asynci ...
- python并发编程&多线程(一)
本篇理论居多,实际操作见: python并发编程&多线程(二) 一 什么是线程 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一 ...
- python并发编程&多进程(一)
本篇理论居多,实际操作见: python并发编程&多进程(二) 一 什么是进程 进程:正在进行的一个过程或者说一个任务.而负责执行任务则是cpu. 举例(单核+多道,实现多个进程的并发执行) ...
- Python并发编程内容回顾
Python并发编程内容回顾 并发编程小结 目录 • 一.到底什么是线程?什么是进程? • 二.Python多线程情况下: • 三.Python多进程的情况下: • 四.为什么有这把GIL锁? • 五 ...
- Python并发编程-并发解决方案概述
Python并发编程-并发解决方案概述 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.并发和并行区别 1>.并行(parallel) 同时做某些事,可以互不干扰的同一个时 ...
- python并发编程之线程/协程
python并发编程之线程/协程 part 4: 异步阻塞例子与生产者消费者模型 同步阻塞 调用函数必须等待结果\cpu没工作input sleep recv accept connect get 同 ...
随机推荐
- Java编程思想:嵌套类
public class Test { public static void main(String[] args) { // Parcell11.test(); // ClassInterface. ...
- SpringMVC面试题:什么是Servlet?
一.什么是servlet? servlet是一个Java编写的程序,此程序是基于http协议的,在服务器端(如Tomcat)运行的,是按照servlet规范编写的一个Java类.客户端发送请求至服务器 ...
- FFT中的一个常见小问题(递推式)
FFT中的一个常见小问题这里不细说FFT的内容,详细内容看这些就足以了解大概了小学生都能看懂的FFT!!!FFT详解补充——FFT中的二进制翻转问题主要是对学习过程中一个容易困扰的小问题进行解释,以便 ...
- AndroidStudio使用genymotion模拟器
安装Genymotion之前首先要安装好virtualbox这个软件 virtual官方网站:https://www.virtualbox.org/ genymotion的官方网站: https:// ...
- 支持微信页面右侧悬浮QQ在线客服
使用方法: 1.将style里的css样式复制到你的样式表中 2.将body中的代码部分拷贝到你需要的地方即可 (js.图片采用绝对路径,不建议修改) <!DOCTYPE html PUBLIC ...
- GitLab-CI 来自动创建 Docker 镜像
1.what is gitlab-ci docker image CI/CD 自动化集成,自动化部署.简单的说就是把代码提交到gitlab管理的同时部署到指定的server,打成docker imag ...
- IO流总结1
一.什么是流? 流就是字节序列的抽象概念,能被连续读取数据的数据源和能被连续写入数据的接收端就是流,流机制是Java及C++中的一个重要机制,通过流我们可以自由地控制文件.内存.IO设备等数据的流向. ...
- CMS简单内容管理系统
架构 NewsDaoSQLServerImpl public class NewsDaoSQLServerImpl extends BaseDao implements NewsDao { publi ...
- Win10系统下安装labelme,json文件批量转化
一.安装环境:windows10,anaconda3,python3.6 由于框架maskrcnn需要json数据集,在没安装labelme环境和跑深度学习之前,我安装的是anacon ...
- phpStudy 升级 MySQL 到 5.7.21
1.备份原来的MySQL 我的路径是D:\phpStudy2018\PHPTutorial\MySQL\bin 修改文件名为MySQL-backup 2.下载新的MySQL 5.7.21 网址:htt ...