python并发编程之线程实例解析

常用用法

t.is_alive()

Python中线程会在一个单独的系统级别线程中执行(比如一个POSIX线程或者一个Windows线程)

这些线程将由操作系统来全权管理。线程一旦启动,将独立执行直到目标函数返回。可以通过查询

一个线程对象的状态,看它是否还在执行t.is_alive()

t.join()

可以把一个线程加入到当前线程,并等待它终止

Python解释器在所有线程都终止后才继续执行代码剩余的部分

daemon

对于需要长时间运行的线程或者需要一直运行的后台任务,可以用后台线程(也称为守护线程)

例:

t=Thread(target=func,args(1,),daemon=True)

t.start()

后台线程无法等待,这些线程会在主线程终止时自动销毁

小结:

后台线程无法等待,不过,这些线程会在主线程终止时自动销毁。你无法结束一个线程,无法给它发送信

号,无法调整它的调度,也无法执行其他高级操作。如果需要这些特性,你需要自己添加。比如说,

如果你需要终止线程,那么这个线程必须通过编程在某个特定点轮询来退出

如果线程执行一些像I/O这样的阻塞操作,那么通过轮询来终止线程将使得线程之间的协调变得非常棘手。

比如,如果一个线程一直阻塞在一个I/O操作上,它就永远无法返回,也就无法检查自己是否已经被结束了。

要正确处理这些问题,需要利用超时循环来小心操作线程。

线程间通信

queue

一个线程向另外一个线程发送数据最安全的方式应该就是queue库中的队列

先看一下使用例子,这里是一个简单的生产者和消费者模型:

from queue import Queue
from threading import Thread
import random
import time
_sentinel = object()
def producer(out_q):
 n = 10
 while n:
 time.sleep(1)
 data = random.randint(0, 10)
 out_q.put(data)
 print("生产者生产了数据{0}".format(data))
 n -= 1
 out_q.put(_sentinel)
def consumer(in_q):
 while True:
 data = in_q.get()
 print("消费者消费了{0}".format(data))
 if data is _sentinel:
  in_q.put(_sentinel)
  break
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))

t1.start()
t2.start()

上述代码中设置了一个特殊值_sentinel用于当获取到这个值的时候终止执行

关于queue的功能有个需要注意的地方:

Queue对象虽然已经包含了必要的锁,主要有q.put和q.get

而q.size(),q.full(),q.empty()等方法不是线程安全的

使用队列进行线程通信是一个单向、不确定的过程。通常情况下,是没有办法知道接收数据的线程是什么时候接收到的数据并开始工作的。但是队列提供了一些基本的特性:q.task_done()和q.join()

如果一个线程需要在另外一个线程处理完特定的数据任务后立即得到通知,可以把要发送的数据和一个Event放到一起使用

关于线程中的Event

线程有一个非常关键的特性:每个线程都是独立运行的,且状态不可预测

如果程序中的其他线程需要通过判断每个线程的状态来确定自己下一步的操作,这时线程同步问题就会比较麻烦。

解决方法:

使用threading库中的Event

Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。

在初始化状态下,event对象中的信号标志被设置为假。

如果有线程等待一个event对象,而这个event的标志为假,这个线程将一直被阻塞知道该标志为真。

一个线程如果把event对象的标志设置为真,就会唤醒所有等待这个event对象的线程。

通过一个代码例子理解:

from threading import Thread, Event
import time
def countdown(n, started_evt):
 print("countdown starting")
 # set将event的标识设置为True
 started_evt.set()
 while n > 0:
 print("T-mins", n)
 n -= 1
 time.sleep(2)

# 初始化的started_evt为False
started_evt = Event()
print("Launching countdown")
t = Thread(target=countdown, args=(10, started_evt,))
t.start()
# 会一直等待直到event的标志为True的时候
started_evt.wait()
print("countdown is running")

而结果,我们也可以看出当线程执行了set之后,才打印running

实际用event对象最好是单次使用,创建一个event对象,让某个线程等待这个对象,一旦对象被设置为Tru,就应该丢弃它,我们虽然可以通过clear()方法重置event对象,但是这个没法确保安全的清理event对象并对它进行重新的赋值。会发生错过事件,死锁等各种问题。

event对象的一个重要特点是它被设置为True时会唤醒所有等待它的线程,如果唤醒单个线程的最好用Condition或信号量Semaphore

和event功能类似的线程中还有一个Condition

关于线程中的Condition

关于Condition官网的一段话:

Aconditionvariableisalwaysassociatedwithsomekindoflock;thiscanbepassedinoronewillbecreatedbydefault.Passingoneinisusefulwhenseveralconditionvariablesmustsharethesamelock.Thelockispartoftheconditionobject:youdon'thavetotrackitseparately.

Othermethodsmustbecalledwiththeassociatedlockheld.Thewait()methodreleasesthelock,andthenblocksuntilanotherthreadawakensitbycallingnotify()ornotify_all().Onceawakened,wait()re-acquiresthelockandreturns.Itisalsopossibletospecifyatimeout.

但是需要注意的是:

notify()andnotify_all()这两个方法,不会释放锁,这意味着线程或者被唤醒的线程不会立刻执行wait()

我们可以通过Conditon对象实现一个周期定时器的功能,每当定时器超时的时候,其他线程都可以检测到,代码例子如下:

import threading
import time
class PeriodicTimer:
 """
 这里做了一个定时器
 """ def __init__(self, interval):
 self._interval = interval
 self._flag = 0
 self._cv = threading.Condition()

 def start(self):
 t = threading.Thread(target=self.run)
 t.daemon = True
 t.start()

 def run(self):
 while True:
  time.sleep(self._interval)
  with self._cv:
  # 这个点还是非常有意思的^=
  self._flag ^= 1
  self._cv.notify_all()

 def wait_for_tick(self):
 with self._cv:
  last_flag = self._flag

  while last_flag == self._flag:
  self._cv.wait()
# 下面两个分别为两个需要定时执行的任务
def countdown(nticks):
 while nticks > 0:
 ptimer.wait_for_tick()
 print('T-minus', nticks)
 nticks -= 1
def countup(last):
 n = 0
 while n < last:
 ptimer.wait_for_tick()
 print('Counting', n)
 n += 1
ptimer = PeriodicTimer(5)
ptimer.start()

threading.Thread(target=countdown, args=(10,)).start()
threading.Thread(target=countup, args=(5,)).start()

关于线程中锁的使用

要在多线程中安全使用可变对象,需要使用threading库中的Lock对象

先看一个关于锁的基本使用:

import threading
class SharedCounter:
 def __init__(self, initial_value=0):
 self._value = initial_value
 self._value_lock = threading.Lock()
 def incr(self,delta = 1):
 with self._value_lock:
  self._value += delta

 def decr(self, delta=1):
 with self._value_lock:
  self._value -= delta

Lock对象和with语句块一起使用可以保证互斥执行,这样每次就只有一个线程可以执行with语句包含的代码块。with语句会在这个代码快执行前自动获取锁,在执行结束后自动释放所。

线程的调度本质上是不确定的,因此,在多线程程序中错误的使用锁机制可能会导致随机数据

损坏或者其他异常错误,我们称之为竞争条件

你可能看到有些“老python程序员”

还是通过_value_lock.acquire()和_value_lock.release(),明显看来

还是with更加方便,不容易出错,毕竟你无法保证那次就忘记释放锁了

为了避免死锁,使用锁机制的程序应该设定每个线程一次只能获取一个锁

threading库中还提供了其他的同步原语:RLock,Semaphore对象。但是这两个使用场景相对来说比较特殊

RLock(可重入锁)可以被同一个线程多次获取,主要用来实现基于检测对象模式的锁定和同步。在使用这种锁的时候,当锁被持有时,只有一个线程可以使用完整的函数或者类中的方法,例子如下:

import threading
class SharedCounter:
 _lock = threading.RLock()
 def __init__(self,initial_value=0):
 self._value = initial_value
 def incr(self,delta=1):
 with SharedCounter._lock:
  self._value += delta
 def decr(self,delta=1):
 with SharedCounter._lock:
  self.incr(-delta)

这个例子中的锁是一个类变量,也就是所有实例共享的类级锁,这样就保证了一次只有一个线程可以调用这个类的方法。与标准锁不同的是已经持有这个锁的方法再调用同样适用这个锁的方法时,无需再次获取锁,例如上面例子中的decr方法。

这种方法的特点是:无论这个类有多少实例都使用一个锁。因此在需要使用大量使用计数器的情况下内存效率更高。

缺点:在程序中使用大量线程并频繁更新计数器时会有竞争用锁的问题。

信号量对象是一个建立在共享计数器基础上的同步原语,如果计数器不为0,with语句讲计数器减1,

线程被允许执行。with语句执行结束后,计数器加1。如果计数器为0,线程将被阻塞,直到其他线程结束并将计数器加1。但是信号量不推荐使用,增加了复杂性,影响程序性能。

所以信号量更适用于哪些需要在线程之间引入信号或者限制的程序。例如限制一段代码的并发量

from threading import Semaphore
import requests
_fetch_url_sema = Semaphore(5)
def fetch_url(url):
 with _fetch_url_sema:
 return requests.get(url)

关于防止死锁的加锁机制

在多线程程序中,死锁问题很大一部分是由于多线程同时获取多个锁造成的。

举个例子:一个线程获取一个第一个锁,在获取第二个锁的时候发生阻塞,那么这个线程就可能阻塞其他线程执行,从而导致整个程序假死。

一种解决方法:为程序中每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁。

import threading
from contextlib import contextmanager

# 存储已经请求锁的信息
_local = threading.local()
@contextmanager
def acquire(*locks):
 # 把锁通过id进行排序
 locks = sorted(locks, key=lambda x: id(x))

 acquired = getattr(_local, 'acquired', [])

 if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
 raise RuntimeError("Lock order Violation")
 acquired.extend(locks)
 _local.acquired = acquired

 try:
 for lock in locks:
  lock.acquire()
 yield
 finally:
 for lock in reversed(locks):
  lock.release()
 del acquired[-len(locks):]
x_lock = threading.Lock()
y_lock = threading.Lock()
def thread_1():
 while True:
 with acquire(x_lock,y_lock):
  print("Thread-1")
def thread_2():
 while True:
 with acquire(y_lock,x_lock):
  print("Thread-2")
t1 = threading.Thread(target=thread_1)
t1.daemon = True
t1.start()

t2 = threading.Thread(target=thread_2)
t2.daemon = True
t2.start()

通过排序,不管以什么样的顺序来请求锁,这些锁都会按照固定的顺序被获取。

这里也用了thread.local()来保存请求锁的信息

同样的这个东西也可以用来保存线程的信息,而这个线程对其他的线程是不可见的

总结

以上就是本文关于python并发编程之线程实例解析的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

(0)

相关推荐

  • python并发编程之多进程、多线程、异步和协程详解

    最近学习python并发,于是对多进程.多线程.异步和协程做了个总结. 一.多线程 多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行.即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行的效果. 多线程相当于一个并发(concunrrency)系统.并发系统一般同时执行多个任务.如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题,比如多线程火车售票系统:两个指令,一个指令检查票是否卖完

  • python实现多线程的方式及多条命令并发执行

    一.概念介绍 Thread 是threading模块中最重要的类之一,可以使用它来创建线程.有两种方式来创建线程:一种是通过继承Thread类,重写它的run方法:另一种是创建一个threading.Thread对象,在它的初始化函数(__init__)中将可调用对象作为参数传入. Thread模块是比较底层的模块,Threading模块是对Thread做了一些包装的,可以更加方便的被使用. 另外在工作时,有时需要让多条命令并发的执行, 而不是顺序执行. 二.代码样例 #!/usr/bin/py

  • Python控制多进程与多线程并发数总结

    一.前言 本来写了脚本用于暴力破解密码,可是1秒钟尝试一个密码2220000个密码我的天,想用多线程可是只会一个for全开,难道开2220000个线程吗?只好学习控制线程数了,官方文档不好看,觉得结构不够清晰,网上找很多文章也都不很清晰,只有for全开线程,没有控制线程数的具体说明,最终终于根据多篇文章和官方文档算是搞明白基础的多线程怎么实现法了,怕长时间不用又忘记,找着麻烦就贴这了,跟我一样新手也可以参照参照. 先说进程和线程的区别: 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共

  • python并发编程之线程实例解析

    常用用法 t.is_alive() Python中线程会在一个单独的系统级别线程中执行(比如一个POSIX线程或者一个Windows线程) 这些线程将由操作系统来全权管理.线程一旦启动,将独立执行直到目标函数返回.可以通过查询 一个线程对象的状态,看它是否还在执行t.is_alive() t.join() 可以把一个线程加入到当前线程,并等待它终止 Python解释器在所有线程都终止后才继续执行代码剩余的部分 daemon 对于需要长时间运行的线程或者需要一直运行的后台任务,可以用后台线程(也称

  • Python并发编程实例教程之线程的玩法

    目录 一.线程基础以及守护进程 二.线程锁(互斥锁) 三.线程锁(递归锁) 四.死锁 五.队列 六.相关面试题 七.判断数据是否安全 八.进程池 & 线程池 总结 一.线程基础以及守护进程 线程是CPU调度的最小单位 全局解释器锁 全局解释器锁GIL(global interpreter lock) 全局解释器锁的出现主要是为了完成垃圾回收机制的回收机制,对不同线程的引用计数的变化记录的更加精准. 全局解释器锁导致了同一个进程中的多个线程只能有一个线程真正被CPU执行. GIL锁每执行700条指

  • Python并发编程线程消息通信机制详解

    目录 1 Event事件 2 Condition 3 Queue队列 4 总结一下 前面我已经向大家介绍了,如何使用创建线程,启动线程.相信大家都会有这样一个想法,线程无非就是创建一下,然后再start()下,实在是太简单了. 可是要知道,在真实的项目中,实际场景可要我们举的例子要复杂的多得多,不同线程的执行可能是有顺序的,或者说他们的执行是有条件的,是要受控制的.如果仅仅依靠前面学的那点浅薄的知识,是远远不够的. 那今天,我们就来探讨一下如何控制线程的触发执行. 要实现对多个线程进行控制,其实

  • Python并发编程协程(Coroutine)之Gevent详解

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporateroutine的缩写,直接翻译为协同的例程,一般我们都简称为协程. 在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程. 进程和协程 下面对比一下进程和协程的相同点和不同点: 相同点: 我们都可以把他们看做是一种执行流,执行流可以挂起,并且后面可以在你挂起的地方恢复执行,这实际上都可以看做是con

  • java编程多线程并发处理实例解析

    本文主要是通过一个银行用户取钱的实例,演示java编程多线程并发处理场景,具体如下. 从一个例子入手:实现一个银行账户取钱场景的实例代码. 第一个类:Account.java 账户类: package cn.edu.byr.test; public class Account { private String accountNo; private double balance; public Account(){ } public Account(String accountNo,double

  • python使用锁访问共享变量实例解析

    本文研究的主要是python使用锁访问共享变量,具体介绍和实现如下. python 做多线程编程时,多个线程若同时访问某个变量,可能会对变量数据造成破坏,pyhon中的threading模块提供了lock对象,lock中的acquire方法用于获取一个锁,而release用于释放一个锁.当一个线程取得锁时,它变获得了共享变量的访问权,此时进入阻塞状态,若其它线程申请访问这个变量,则必须等到这个线程调用release方法释放这个锁.下面是python中使用锁的实例: #!/usr/bin/env

  • Java多线程并发编程和锁原理解析

    这篇文章主要介绍了Java多线程并发编程和锁原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等方案后,问题得到解决. 加锁方案见下文. 二.乐观锁 & 悲观锁 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • Java并发编程之线程之间的共享和协作

    一.线程间的共享 1.1 ynchronized内置锁 用处 Java支持多个线程同时访问一个对象或者对象的成员变量 关键字synchronized可以修饰方法或者以同步块的形式来进行使用 它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中 它保证了线程对变量访问的可见性和排他性(原子性.可见性.有序性),又称为内置锁机制. 对象锁和类锁 对象锁是用于对象实例方法,或者一个对象实例上的 类锁是用于类的静态方法或者一个类的class对象上的 类的对象实例可以有很多个,但是每个类只有

  • Python并发编程之未来模块Futures

    目录 区分并发和并行 并发编程之Futures 到底什么是Futures? 为什么多线程每次只有一个线程执行? 总结 不论是哪一种语言,并发编程都是一项非常重要的技巧.比如我们上一章用的爬虫,就被广泛用在工业的各个领域.我们每天在各个网站.App上获取的新闻信息,很大一部分都是通过并发编程版本的爬虫获得的. 正确并合理的使用并发编程,无疑会给我们的程序带来极大性能上的提升.今天我们就一起学习Python中的并发编程——Futures. 区分并发和并行 我们在学习并发编程时,常常会听到两个词:并发

  • java并发编程_线程池的使用方法(详解)

    一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

随机推荐