Python多线程与多进程(一)

多线程

多线程是程序在同样的上下文中同时运行多条线程的能力。这些线程共享同一个进程的资源,可以在并发模式(单核处理器)或并行模式(多核处理器)下执行多个任务

多线程有以下几个优点:

  • 持续响应:在单线程的程序中,执行一个长期运行的任务可能会导致程序的冻结。多线程可以把这个长期运行的任务放在一个线程中,在程序并发的运行任务时可以持续响应客户的需求
  • 更快的执行速度:在多核处理器的操作系统上,多线程可以通过真正的并行提高程序的运行速度
  • 较低的资源消耗:利用线程模式,程序可以利用一个进程内的资源响应多个请求
  • 更简单的状态共享与进程间的通信机制:由于线程都共享同一资源和内存空间,因此线程之间的通比进程间通信简单
  • 并行化:多处理器系统可以实现多线程的每个线程独立运行

但是多线程也有以下几个缺点:

  • 线程同步:由于多个线程是在同一数据上运行的,所以需要引入一些机制预防竞态条件
  • 问题线程导致集体崩溃:虽然多个线程可以独立运行,但一旦某个线程出现问题,也可能造成整个进程崩溃
  • 死锁:这是线程操作的常见问题。通常,线程执行任务时会锁住正在使用的资源,当一个线程开始等待另一个线程资源释放,而另一个线程同时也要等待第一个线程释放资源时,就发生了死锁

通常,多线程技术完全可以在多处理器上实现并行计算。但是Python的官方版本(CPython)有一个GIL限制,GIL会阻止多个线程同时运行Python的字节码,这就不是真正的并行了。假如你的系统有6个处理器,多线程可以把CPU跑到

600%,然而,你能看到的只有100%,甚至更慢一点,这都是GIL造成的

CPython的GIL是有必要的,因为CPython的内存管理不是线程安全的。因此,为了让每个任务都按顺序进行,它需要确保运行过程中内存不被干扰。它可以更快的运行单线程程序,简化C语言扩展库的使用方法,因为它不需要考虑多线程问题。

但是,GIL是可以用一些办法绕过的。例如,由于GIL只阻止多个线程同时运行Python的字节码,所以可以用C语言写程序,然后用Python封装。这样,在程序运行过程中GIL就不会干扰多线程并发了

另一个GIL不影响性能的示例就是网络服务器了,服务器大部分时间都在读数据包,而当发生IO等待时,会尝试释放GIL。这种情况下,增加线程可以读取更多的包,虽然这并不是真正的并行。这样做可以增加服务器的性能,但是不会影响速度。

用_thread模块创建线程

我们先用一个例子快速演示_thread模块的用法:_thread模块提供了start_new_thread方法。我们可以向里面传入以下参数:

  • 目标函数:里面包含我们要运行的代码,一旦函数返回值,线程就停止运行
  • 参数:即执行目标函数所需的参数,一般以元组的形式传入
import _thread
import time

def print_time(thread_name, delay):
    count = 0
    while count < 5:
        time.sleep(delay)
        count += 1
        print("%s:%s" % (thread_name, time.ctime(time.time())))

try:
    _thread.start_new_thread(print_time, ("thread-A", 1))
    _thread.start_new_thread(print_time, ("thread-B", 2))
except:
    print("Error: unable to start thread")

while 1:
    pass

  

运行结果:

thread-A:Sun Jul  8 07:39:27 2018
thread-B:Sun Jul  8 07:39:28 2018
thread-A:Sun Jul  8 07:39:28 2018
thread-A:Sun Jul  8 07:39:29 2018
thread-B:Sun Jul  8 07:39:30 2018
thread-A:Sun Jul  8 07:39:30 2018
thread-A:Sun Jul  8 07:39:31 2018
thread-B:Sun Jul  8 07:39:32 2018
thread-B:Sun Jul  8 07:39:34 2018
thread-B:Sun Jul  8 07:39:36 2018

  

上面的例子很简单,线程A和线程B是并发执行的。

_thread模块还提供了一些容易使用的线程原生接口:

  • _thread.interrupt_main():这个方法可以向主线程发送中断异常,就像通过键盘向程序输入CTRL+C一样,我们修改print_time方法,当count为2,休眠时间delay为2向主线程发送中断异常

    def print_time(thread_name, delay):
        count = 0
        while count < 5:
            time.sleep(delay)
            count += 1
            if count == 2 and delay == 2:
                _thread.interrupt_main()
            print("%s:%s" % (thread_name, time.ctime(time.time())))
    

    运行结果:

    thread-A:Sun Jul  8 09:12:57 2018
    thread-B:Sun Jul  8 09:12:58 2018
    thread-A:Sun Jul  8 09:12:58 2018
    thread-A:Sun Jul  8 09:12:59 2018
    thread-B:Sun Jul  8 09:13:00 2018
    Traceback (most recent call last):
      File "D:/pypath/hello/test3/test01.py", line 22, in <module>
        pass
    KeyboardInterrupt
    

        

  • exit:这个方法会从后台退出程序,它的优点是中断线程时不会引起其他异常

    def print_time(thread_name, delay):
        count = 0
        while count < 5:
            time.sleep(delay)
            count += 1
            if count == 2 and delay == 2:
                _thread.exit()
            print("%s:%s" % (thread_name, time.ctime(time.time())))
    

    运行结果:

    thread-A:Sun Jul  8 09:15:51 2018
    thread-B:Sun Jul  8 09:15:52 2018
    thread-A:Sun Jul  8 09:15:52 2018
    thread-A:Sun Jul  8 09:15:53 2018
    thread-A:Sun Jul  8 09:15:54 2018
    thread-A:Sun Jul  8 09:15:55 2018
    

      

allocate_lock方法可以为线程返回一个线程锁,这个锁可以保护某一代码块从开始运行到运行结束只有一个线程,线程锁对象有三个方法:

  • acquire:这个方法的主要作用是为当前的线程请求一把线程锁。它接受一个可选的整型参数,如果参数是0,那么线程锁一旦被请求则立即获取,不需要等待,如果参数不是0,则表示线程可以等待锁
  • release:这个方法会释放线程锁,让下一个线程获取
  • locked:如果线程锁被某个线程获取,就返回True,否则为False

下面这段代码用10个线程对一个全局变量增加值,因此,理想情况下,全局变量的值应该是10:

import _thread
import time

global_values = 0

def run(thread_name):
    global global_values
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1

for i in range(10):
    _thread.start_new_thread(run, ("thread-(%s)" % str(i),))

time.sleep(3)
print("global_values:%s" % global_values)

  

运行结果:

thread-(0) with value 0
thread-(1) with value 0
thread-(2) with value 0
thread-(4) with value 0
thread-(6) with value 0
thread-(8) with value 0
thread-(7) with value 0
thread-(5) with value 0
thread-(3) with value 0
thread-(9) with value 1
global_values:2

    

但是很遗憾,我们没有得到我们希望的结果,相反,程序运行的结果和我们希望的结果差距更远。造成这样的原因,都是因为多个线程操作同一变量或同一代码块导致有的线程不能读到最新的值,甚至是把旧值的运算结果赋给全部局变量

现在,让我们修改一下原先的代码:

import _thread
import time

global_values = 0

def run(thread_name, lock):
    global global_values
    lock.acquire()
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1
    lock.release()

lock = _thread.allocate_lock()

for i in range(10):
    _thread.start_new_thread(run, ("thread-(%s)" % str(i), lock))

time.sleep(3)
print("global_values:%s" % global_values)

  

运行结果:

thread-(0) with value 0
thread-(2) with value 1
thread-(4) with value 2
thread-(5) with value 3
thread-(3) with value 4
thread-(6) with value 5
thread-(1) with value 6
thread-(7) with value 7
thread-(8) with value 8
thread-(9) with value 9

  

现在可以看到,线程的执行顺序依旧是乱序的,但全局变量的值是逐个递增的

_thread还有其他一些方法:

  • _thread.get_ident():这个方法会返回一个非0的整数,代表当前活动线程的id。这个整数会在线程结束或退出后收回,因此在整个程序的生命周期中它并不是唯一
  • _thread.stack_size(size):size这个参数是可选项,可在代码创建新线程时设置或返回线程栈的容量,这个容量可以是0,或者至少32KB,具体由操作系统决定

用threading模块创建线程

这是目前Python中处理线程普遍推荐的模块,这个模块提供了更完善和高级的接口,我们尝试将前面的示例转化成threading模块的形式:

import threading
import time

global_values = 0

def run(thread_name, lock):
    global global_values
    lock.acquire()
    local_copy = global_values
    print("%s with value %s" % (thread_name, local_copy))
    global_values = local_copy + 1
    lock.release()

lock = threading.Lock()

for i in range(10):
    t = threading.Thread(target=run, args=("thread-(%s)" % str(i), lock))
    t.start()

time.sleep(3)
print("global_values:%s" % global_values)

  

对于更复杂的情况,如果要更好地封装线程的行为,我们可能需要创建自己的线程类,这里需要注意几点:

  • 需要继承thread.Thread类
  • 需要改写run方法,也可以使用__init__方法
  • 如果改写初始化方法__init__,需要在一开始调用父类的初始化方法Thread.__init__
  • 当线程的run方法停止或抛出未处理的异常时,线程将停止,因此要提前设计好方法
  • 可以用初始化方法的name参数名称命名你的线程
import threading
import time

class MyThread(threading.Thread):

    def __init__(self, count):
        threading.Thread.__init__(self)
        self.total = count

    def run(self):
        for i in range(self.total):
            time.sleep(1)
            print("Thread:%s - %s" % (self.name, i))

t = MyThread(2)
t2 = MyThread(3)
t.start()
t2.start()

print("finish")

  

运行结果:

finish
Thread:Thread-2 - 0
Thread:Thread-1 - 0
Thread:Thread-2 - 1
Thread:Thread-1 - 1
Thread:Thread-2 - 2

  

注意上面主线程先打印了finish,之后才打印其他线程里面的print语句,这并不是什么大问题,但下面的情况就有问题了:

f = open("content.txt", "w+")
t = MyThread(2, f)
t2 = MyThread(3, f)
t.start()
t2.start()
f.close()

  

我们假设在MyThread中会将打印的语句写入content.txt,但这段代码是会出问题的,因为在开启其他线程前,主线程可能会先关闭文件处理器,如果想避免这种情况,应该使用join方法,join方法会使得被调用的线程执行完毕后,在能返回原先的线程继续执行下去:

f = open("content.txt", "w+")
t = MyThread(2, f)
t2 = MyThread(3, f)
t.start()
t2.start()
t.join()
t2.join()
f.close()
print("finish")

  

join方法还支持一个可选参数:时限(浮点数或None),以秒为单位。但是join返回值是None。因此,要检查操作是否已超时,需要在join方法返回后查看线程的激活状态,如果线程的状态是激活的,操作就超时了

再来看一个示例,它检查一组网站的请求状态码:

from urllib.request import urlopen

sites = [
    "https://www.baidu.com/",
    "http://www.sina.com.cn/",
    "http://www.qq.com/"
]

def check_http_status(url):
    return urlopen(url).getcode()

http_status = {}
for url in sites:
    http_status[url] = check_http_status(url)

for key, value in http_status.items():
    print("%s %s" % (key, value))

  

运行结果:

# time python3 test01.py
https://www.baidu.com/ 200
http://www.sina.com.cn/ 200
http://www.qq.com/ 200

real	0m1.669s
user	0m0.143s
sys	0m0.026s

  

现在,我们尝试着把IO操作函数转变为一个线程来优化代码:

from urllib.request import urlopen
import threading

sites = [
    "https://www.baidu.com/",
    "http://www.sina.com.cn/",
    "http://www.qq.com/"
]

class HttpStatusChecker(threading.Thread):
    def __init__(self, url):
        threading.Thread.__init__(self)
        self.url = url
        self.status = None

    def run(self):
        self.status = urlopen(self.url).getcode()

threads = []

http_status = {}
for url in sites:
    t = HttpStatusChecker(url)
    t.start()
    threads.append(t)

for t in threads:
    t.join()

for t in threads:
    print("%s %s" % (t.url, t.status))

  

运行结果:

# time python3 test01.py
https://www.baidu.com/ 200
http://www.sina.com.cn/ 200
http://www.qq.com/ 200

real	0m0.237s
user	0m0.110s
sys	0m0.019s

  

显然,线程版的程序更快,运行速度几乎是上一版的8倍,性能改善十分显著

通过Event对象实现线程间通信

虽然线程通常是作为独立运行或并行的任务,但是有时也会出现线程间通信的需求,threading模块提供了事件(event)对象实现线程间通信,它包含一个内部标记,以及可以使用set()和clear()方法的调用线程

Event类的接口很简单,它支持的方法如下:

  • is_set:如果事件设置了内部标记,就返回True
  • set:把内部标记设置为True。它可以唤醒等待被设置标记的所有线程,调用wait()方法的线程将不再被阻塞
  • clear:重置内部标记。调用wait方法的线程,在调用set()方法之前都将被阻塞
  • wait:在事件的内部标记被设置好之前,使用这个方法会一直阻塞线程调用,这个方法支持一个可选参数,作为等待时限(timeout)。如果等待时限非0,则线程会在时限内被一直阻塞

让我们用线程事件对象来演示一个简单的线程通信示例,它们可以轮流打印字符串。两个线程共享同一个事件对象。在while循环中,每次循环时,一个线程设置标记,另一个线程重置标记。

import threading
import time

class ThreadA(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        count = 0
        while count < 6:
            time.sleep(1)
            if self.event.is_set():
                print("A")
                self.event.clear()
            count += 1

class ThreadB(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.event = event

    def run(self):
        count = 0
        while count < 6:
            time.sleep(1)
            if not self.event.is_set():
                print("B")
                self.event.set()
            count += 1

event = threading.Event()
ta = ThreadA(event)
tb = ThreadB(event)
ta.start()
tb.start()

  

运行结果:

B
A
B
A
B
A
B
A
B
A
B

  

下面总结一下Python多线程的使用时机:

使用多线程:

  • 频繁的IO操作
  • 并行任务可以通过并发解决
  • GUI开发

不使用多线程:

  • 大量的CPU操作任务
  • 程序必须利用多核心操作系统

 多进程

由于GIL的存在,Python的多线程并没有实现真正的并行。因此,一些问题使用threading模块并不能解决

不过Python为并行提供了一个替代方法:多进程。在多进程里,线程被换成一个个子进程。每个进程都运作着各自的GIL(这样Python就可以并行开启多个进程,没有数量限制)。需要明确的是,线程都是同一个进程的组成部分,它们共享同一块内存、存储空间和计算资源。而进程却不会与它们的父进程共享内存,因此进程间通信比线程间通信更为复杂

多进程相比多线程优缺点如下:

优点 缺点
可以使用多核操作系统 更多的内存消耗
进程使用独立的内存空间,避免竞态问题 进程间的数据共享变得更加困难
子进程容易中断 进程间通信比线程困难
避开GIL限制  

  

原文地址:https://www.cnblogs.com/beiluowuzheng/p/9278619.html

时间: 2024-10-09 05:02:09

Python多线程与多进程(一)的相关文章

Python多线程和多进程谁更快?

python多进程和多线程谁更快 python3.6 threading和multiprocessing 四核+三星250G-850-SSD 自从用多进程和多线程进行编程,一致没搞懂到底谁更快.网上很多都说python多进程更快,因为GIL(全局解释器锁).但是我在写代码的时候,测试时间却是多线程更快,所以这到底是怎么回事?最近再做分词工作,原来的代码速度太慢,想提速,所以来探求一下有效方法(文末有代码和效果图) 这里先来一张程序的结果图,说明线程和进程谁更快 一些定义 并行是指两个或者多个事件

基于Windows平台的Python多线程及多进程学习小结

python多线程及多进程对于不同平台有不同的工具(platform-specific tools),如os.fork仅在Unix上可用,而windows不可用,该文仅针对windows平台可用的工具进行总结. 1.多线程 单线程中,如果某一任务(代码块)是long-time running的,则必须等待该任务(代码块)结束,才可以对下一个任务进行操作,为解决long-time 任务的block问题,可将创建多个线程,间隔选择多线程进行操作.python 中多线程常用的库为_thread,thr

python多线程、多进程以及GIL

多线程 使用threading模块创建线程 传入一个函数 这种方式是最基本的,即调用threading中的Thread类的构造函数,然后指定参数target=func,再使用返回的Thread的实例调用start()方法,即开始运行该线程,该线程将执行函数func,当然,如果func需要参数,可以在Thread的构造函数中传入参数args=(-).示例代码如下 import threading #用于线程执行的函数 def counter(n): cnt = 0; for i in xrange

python多线程、多进程、协程的使用

本文主要介绍多线程.多进程.协程的最常见使用,每个的详细说明与介绍有时间会在以后的随笔中体现. 一.多线程 1.python通过两个标准库thread和threading提供对线程的支持.thread提供了低级别的.原始的线程以及一个简单的锁.threading通过对thread模块进行二次封装,提供了更方便的API来操作线程.接下来只介绍threading的常见用法. 2.使用 import threading import time def Traversal_5(interval): fo

python 多线程、多进程

一.首先说下多线程.多进程用途及异同点,另外还涉及到队列的,memcache.redis的操作等: 1.在python中,如果一个程序是IO密集的操作,使用多线程:运算密集的操作使用多进程. 但是,其实在python中,只支持一个cpu的多线程,多个任务是切换执行的,并不能并行执行,所以有的时候,多线程并不比单线程要快,在我们的理解中,下意识的就会认为 多线程肯定比单线程要快,其实不然,多线程只会在有线程阻塞的情况下才会起到效果,下面我们来看一个实例: 1 import os,sys,json

python 多线程和多进程

多线程与多进程 知识预览 一 进程与线程的概念 二 threading模块 三 multiprocessing模块 四 协程 五 IO模型 回到顶部 一 进程与线程的概念 1.1 进程 考虑一个场景:浏览器,网易云音乐以及notepad++ 三个软件只能顺序执行是怎样一种场景呢?另外,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源.你是不是已经想到在程序A读取数据的过程中,

python多线程,多进程编程。

程,是目前计算机中为应用程序分配资源的最小单位: 进程,是目前计算机中运行应用程序的最小单位: 在实际系统中,其实进程都是被分为进程来实现的,所以参与时间片轮转的是线程: 但是管理应用程序的资源的单位和任务调度的单位都是进程.更像是一个逻辑概念. 线程是进程分出来的更精细的单位,线程间的上下文切换比进程间的上下文切换,要快很多. 多进程与多核,这个概念很奇怪,因为进程是不会直接在核心上运行的. 多线程与多核,涉及一个内核线程与用户线程的对应关系. 内核线程(Kernel Thread),一般与核

Python 多线程 和 多进程的CPU使用情况进行对比

# 多进程 这是没跑多进程之前的使用情况 跑了2个多进程之后: 使用率 65%, 跑了4个多进程后: CPU使用率:100% --------------------------------------------------分割线------------------------------------------------------------ 多线程: 开启了4个线程,但使用率始终是百分之20多,因为在Python里,永远只有一个线程在工作 -----------------------

Python多线程,多进程,并行,并发,异步编程

Python并发与并行的新手指南:http://python.jobbole.com/81260/ Python 中的多线程,多进程,并发,并行,同步,通信:https://blog.csdn.net/timemachine119/article/details/54091323 python进阶笔记 thread 和 threading模块学习:https://www.cnblogs.com/forward-wang/p/5970640.html Python 中的多线程,多进程,并发,并行,