python学习笔记(九)——线程与进程

一、线程

Python 中为我们提供了两个模块来创建线程。

  • _thread
  • threading

thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python 中不能再使用"thread" 模块。为了兼容性,Python 将 thread 重命名为 “_thread”。

相对 _thread 模块来说, threading 模块更加高级也更加常用。

多线程

创建多线程步骤:

  1. 导入线程库
  2. 创建任务函数
  3. 创建线程对象
  4. 启动线程

实例:

import threading,time

def task():
    for i in range(1,11):
        time.sleep(1)
        print(threading.current_thread().name,end="")
        print(":output >%d"%i)

print(time.ctime())
# 创建两个线程
pt1=threading.Thread( target=task )
pt2=threading.Thread( target=task )
# 输出线程名
print(threading.current_thread().name)
# 启动线程
pt1.start()
pt2.start()
pt1.join()
pt2.join()
print(time.ctime())

输出:

Sat Mar 14 16:07:12 2020
MainThread
Thread-2:output >1
Thread-1:output >1
Thread-1Thread-2:output >2
:output >2
Thread-1Thread-2:output >3
:output >3
Thread-1Thread-2:output >4
:output >4
Thread-1:output >5
Thread-2:output >5
Thread-1Thread-2:output >6
:output >6
Thread-2:output >7
Thread-1:output >7
Thread-2Thread-1:output >8
:output >8
Thread-2:output >9
Thread-1:output >9
Thread-1:output >10
Thread-2:output >10
Sat Mar 14 16:07:22 2020

可以看到一共用了10秒时间,两个线程都完成了自己的任务。

使用 threading.Thread( ) 创建线程时,可以传入六个参数,比如线程名 name,任务函数参数 args等。


    def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):

其中,如name、daemon等参数除了在创建线程时指定以外,还可以通过对象调用函数设置。 pt.setName()、pt.setDaemon()。

线程同步

在多线程中会常会发生多个线程同时访问同一个资源的情况,造成线程不安全。而我们要求线程对临界资源的操作是必须是原子操作,因此我们可以通过线程锁来实现线程安全。

使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法进行加锁和解锁。

lock=threading.RLock()		# 创建一个锁🔒
lock.acquire()		# 加锁
lock.release()		# 解锁

把每次只允许一个线程操作的数据,放到 acquire 和 release 方法之间进行操作。

全局解释器锁GIL

在非python环境中,单核情况下,同时只能有一一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一 一个。拿不到通行证的线程,就不允许进入CPU执行。GIL 只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一-时间只能有- -个线程拿到数据。而在pypy和ipython中是没有GIL的。

所以,在Python中,可以使用多线程,但不要指望能有效利用多核。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响

二、进程

使用多进程同样可以完成多线程的工作,我们将之前的多线程程序改成多进程程序。

import multiprocessing
import time

def task():
    for i in range(1,11):
        time.sleep(1)
        print(multiprocessing.current_process().name,__name__,end="")
        print(":output >%d"%i)

if __name__=="__main__":
    print(time.ctime())
    p1=multiprocessing.Process(target=task)
    p2=multiprocessing.Process(target=task)
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print(time.ctime())

输出:

Sat Mar 14 17:25:31 2020
Process-1 __mp_main__:output >1
Process-2 __mp_main__:output >1
Process-1 __mp_main__:output >2
Process-2 __mp_main__:output >2
Process-1 __mp_main__:output >3
Process-2 __mp_main__:output >3
Process-1 __mp_main__:output >4
Process-2 __mp_main__:output >4
Process-1 __mp_main__:output >5
Process-2 __mp_main__:output >5
Process-1 __mp_main__:output >6
Process-2 __mp_main__:output >6
Process-1 __mp_main__:output >7
Process-2 __mp_main__:output >7
Process-1 __mp_main__:output >8
Process-2 __mp_main__:output >8
Process-1 __mp_main__:output >9
Process-2 __mp_main__:output >9
Process-1 __mp_main__:output >10
Process-2 __mp_main__:output >10
Sat Mar 14 17:25:41 2020
进程池

通过进程池 pool 可以批量的创建大量的进程。

import multiprocessing
import os
import time

def task(i):
    print("线程%d 执行:  pid=%s"%(i,os.getpid()))
    time.sleep(1)
    return i

def back(n):
    print("任务%d已完成"%n)

if __name__=="__main__":
    mypool=multiprocessing.Pool(5)  # 线程池大小
    for i in range(1,21):           # 20个线程任务
        mypool.apply_async(func=task,args=(i,))
    mypool.close()
    mypool.join()

apply_async() 会等待线程池中有空闲时依次执行线程任务。其中,callback 参数是回调函数。

进程池中回调函数callback作用是:进程池中任何一个任务一旦处理完了,就立即告知主进程,主进程则调用一个函数去处理该结果,它可以用来接收进程任务的返回值,判断其执行的结果。

输出结果:

线程1 执行:  pid=12372
线程2 执行:  pid=11236
线程3 执行:  pid=15144
线程4 执行:  pid=8332
线程5 执行:  pid=16732
线程6 执行:  pid=12372
任务1已完成
线程7 执行:  pid=11236
任务2已完成
线程8 执行:  pid=15144
任务3已完成
线程9 执行:  pid=8332
任务4已完成
线程10 执行:  pid=16732
任务5已完成
线程11 执行:  pid=12372
任务6已完成
线程12 执行:  pid=11236
任务7已完成
线程13 执行:  pid=15144
任务8已完成
线程14 执行:  pid=8332
任务9已完成
线程15 执行:  pid=16732
任务10已完成
线程16 执行:  pid=12372
任务11已完成
线程17 执行:  pid=11236
任务12已完成
线程18 执行:  pid=15144
任务13已完成
线程19 执行:  pid=8332
任务14已完成
线程20 执行:  pid=16732
任务15已完成
任务16已完成
任务17已完成
任务18已完成
任务19已完成
任务20已完成

需要注意的是,在使用线程池时应注意将pool.close() 先于 pool.join() 调用。因为 join() 会让主进程阻塞等待子进程全部执行完之后再执行,close() 的作用是关闭pool,使其不在接受新的(主进程)任务。所以,两个顺序可不能颠倒了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章