Python之多线程与多进程(一)

多线程python

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

多线程有如下几个优势:bash

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

可是多线程也有如下几个缺点:服务器

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

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

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

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

可是,GIL是能够用一些办法绕过的。例如,因为GIL只阻止多个线程同时运行Python的字节码,因此能够用C语言写程序,而后用Python封装。这样,在程序运行过程当中GIL就不会干扰多线程并发了app

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

用_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限制
相关文章
相关标签/搜索