了解python之进程与线程

了解python之进程与线程

// 本文虽然叫作 “了解python进程与线程”, 但还是有点难度的。 可以先查阅另外一篇文字,快速入门。 Python快速入门多线程与多进程

 

1.进程

进程(Process,有时也被称为重量级进程)是程序的一次执行,每个进程都有自己的地址空间、内存、数据栈以及记录运行轨迹的辅助数据,操作系统管理运行的所有进程,并为这些进程公平分配时间。进程可以通过fork和spawn操作完成其他任务。因为各个进程都有自己的内存空间和数据栈等,所以只能使用进程间通信(IPC),而不能直接共享信息。

 

2.线程:

线程(Thread,有时被称为轻量级的进程)跟进程有些相似,不同的是所有线程运行在同一个进程中,共享运行环境。线程有开始、顺序执行和结束3个部分,有一个自己的指令指针,记录运行到什么地方。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更好更方便的共享数据和相互通信。

线程一般是并发执行的,正是由于这种并行和数据共享的机制,使得多个任务的合作变的可能。实际上,在单CPU系统中,真正的并发并不可能,每个线程会被安排每次只运行一小会,然后就把CPU让出来,让其他线程运行。

在进程的整个运行过程中,每个线程都只做自己的事,需要在跟其他线程共享运行结果。多个线程共享同一片数据不是完全没有危险的,由于数据访问顺序不一样,因此可能会导致数据结果不一致的问题,这叫做竞态条件。大多数线程库都带有一系列同步原语,用于控制线程的执行和数据访问。

 

每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。 指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。

  • 线程可以被抢占(中断)。
  • 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。

线程可以分为:

  • 内核线程:由操作系统内核创建和撤销。
  • 用户线程:不需要内核支持而在用户程序中实现的线程。

Python3 线程中常用的两个模块为:

  • _thread
  • threading(推荐使用)

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

3.多线程和多进程


什么叫多任务?
简单的说就是系统可以同时运行多个任务。比如,一边用浏览器上网,一边听音乐,一边聊天,这就是多任务。假如现在已经有三个任务在运行了,查看任务管理器,可以看到很多任务在后台运行,只是在桌面没有显示。对于操作系统来说,一个任务就是一个进程,开启多个任务就是多进程。

有些进程不止可以做一件事,比方说word可以是同时打字、拼写检查、打印等。在一个进程内部,要同时做多件事,就需要同时运行多个线程

 

多线程类似于同时执行多个不同的程序,多线程运行有以下几个优点:
1)使用线程可以把占据长时间的程序中的任务放到后台去处理
2)用户界面可以更加吸引人,比如用户单击一个按钮,用于触发某些事件的处理,可以弹出一个进度条显示处理的进度
3)程序的运行速度可能加快
4)在实现一些等待任务(如用户输入、文件读写、网络收发数据等)的时候,使用多线程更加有用。在这种情况下,我们可以释放一些珍贵的资源(如内存占用等)。

 

线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存在进程中,由进程提供多个线程执行控制。

由于每个进程至少要干一件事,因此一个进程至少有一个线程。多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间相互切换,让每个线程都短暂交互运行,看起来就像同时执行一样。同时执行多线程需要多核CPU才能实现。

 

如果要同时执行多个任务,怎们办?

  • 第一种方法:启动多个进程,每个进程虽然只有一个线程,但是多个进程可以一起执行多个任务。
  • 第二种方法:启动一个进程,在一个进程内启动多个线程,这样多个线程也可以一起执行多个任务
  • 第三种方法:启动多个进程,每个进程在启动多个线程,这样同时执行的任务就更多了,这种模式过于复杂,实际上很少采用。

同时执行多个任务时,各个任务之间需要相互通信和协调,有时候任务1需要暂停,等待任务2完成后才能继续执行;有时任务3和任务4不能同时执行。多进程、多线程程序的复杂度远远高于前面的单进程、单线程的程序。

综上所述:

  • 多线程是多个相关联的线程的组合,多进程是多个相互独立的进程的组合。
  • 线程是最小的执行单元,进程至少由一个线程组成。

 

4.并发和并行

一个程序在计算机中运行,其底层是处理器通过运行一条条的指令来实现的。

并发,英文叫作 concurrency它是指同一时刻只能有一条指令执行,但是多个线程的对应的指令被快速轮换地执行。比如一个处理器,它先执行线程 A 的指令一段时间,再执行线程 B 的指令一段时间,再切回到线程 A 执行一段时间。

由于处理器执行指令的速度和切换的速度非常非常快,人完全感知不到计算机在这个过程中有多个线程切换上下文执行的操作,这就使得宏观上看起来多个线程在同时运行。但微观上只是这个处理器在连续不断地在多个线程之间切换和执行,每个线程的执行一定会占用这个处理器一个时间片段,同一时刻,其实只有一个线程在执行。

 

并行,英文叫作 parallel它是指同一时刻,有多条指令在多个处理器上同时执行,并行必须要依赖于多个处理器。不论是从宏观上还是微观上,多个线程都是在同一时刻一起执行的。

并行只能在多处理器系统中存在,如果我们的计算机处理器只有一个核,那就不可能实现并行。而并发在单处理器和多处理器系统中都是可以存在的,因为仅靠一个核,就可以实现并发

 

举个例子,比如系统处理器需要同时运行多个线程。如果系统处理器只有一个核,那它只能通过并发的方式来运行这些线程。如果系统处理器有多个核,当一个核在执行一个线程时,另一个核可以执行另一个线程,这样这两个线程就实现了并行执行,当然其他的线程也可能和另外的线程处在同一个核上执行,它们之间就是并发执行。具体的执行方式,就取决于操作系统的调度了。

 

5.全局解释器锁(GIL)

python虚拟机的访问由全局解释器锁控制,这个锁能保证同一个时刻只有一个线程运行。

在多线程环境中,python虚拟机按照以下方式执行:
1)设置GIL
2)切换到一个线程运行
3)运行指定数量的字节编码指令或者线程主动让出控制(可以调用time.sleep())
4)把线程设置成睡眠状态
5)解锁GIL
6)再次重复以上所有步骤

在调用外部代码时,GIL将被锁定。直到这个函数结束为止,编写扩展的程序员可以主动解锁GIL。

简言之,无论你启动多少个线程,你有多少个cpu,python在执行的时候在同一时刻只允许同一个线程运行。

6.退出线程

当一个线程结束计算,它就退出了。线程可以调用_thread.exit()等退出函数,也可以使用python退出进程的标准方法(sys.exit()或者抛出一个SystemExit异常),不能直接kill掉一个线程。

不建议使用_thread模块,最重要的原因是:当主线程退出时,其他线程如果没有被清除就会退出。另一个模块threading能确保所有的重要的子线程都退出后,进程才会结束。
 

python中的线程模块:
python提供了几个用于多线程编程的模块,包括_thread、threading和Queue等。_thread和threading模块允许程序员创建和管理线程。_thread模块提供了基本线程和锁的支持,threading提供了更高级别、功能更强的线程管理功能。Queue模块允许允许用户创建一个可以用于多线程之间共享数据的队列数据结构。

 

为什么建议不使用_thread模块?

  • threading模块更先进,对线程的支持更完善,而且使用_thread模块里的属性有可能跟threading冲突;
  • 低级别的_thread模块同步原语很少,threading很多;
  • _thread模块在主线程结束时,所有线程都会被强制结束,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出;

 

多线程的缺点:
由于GIL的存在,python多线程只能有一个线程执行,无法利用多核cpu实现并行执行的特点。由于线程的切换需要时间的,多线程使用不当程序的执行速度还不如单线程。

 

7.python的_thread模块

python中调用_thread模块中的start_new_thread()函数产生一个新的线程。_thread语法如下:

_thread.start_new_thread(function,args[,kwargs])

参数说明:

  • function-线程函数
  • args-传递给线程函数的参数,必须是元组(tuple)
  • kwargs-可选参数

_thread模块除了产生线程外,还提供基本同步数据结构锁对象lock object,也叫做原语锁、简单锁、互斥锁、互斥量、二值信号量)。同步原语与线程管理是密不可分的。

 

_thread模块的主要方法:

  • _thread.start_new_thread(function,args[,kwargs])
  • _thread.allocate_lock()                                    #分配锁对象
  • _thread.exit()                                                   #退出线程
  • lock.acquire(waitflag=1,timeout=1)                 #获取锁对象
  • lock.locked()                                                   #如果获取了锁对象,返回true,否则返回false
  • lock.release()                                                 #释放锁
  • _thread.LockType()                                       #锁对象的类型
  • _thread.get_ident()                                       #获取线程标识符
  • _thread.TIMEOUT_MAX                              #locak.acquire的最大时间,超过这个时间引发OverflowError
  • _thread.interrupt_main()                              #引发主线程keyboardInterrupt错误,子线程可以用这个函数终止主线程

 

例1,多线程实例

import _thread
import time

#为线程定义的函数
def print_time(threadName,delay):
    count=0
    while count <5:
        time.sleep(delay)
        count +=1   #等价于count=count+1
        print('%s: %s' % (threadName,time.ctime(time.time())))

#创建两个线程
try:
    _thread.start_new_thread(print_time,("Thread-1",2))
    _thread.start_new_thread(print_time,('Thread-2',6))
except:
    print('Error: 线程无法启动')

while 1:
    pass


##输出结果:
Thread-1: Wed May 26 09:25:49 2021
Thread-1: Wed May 26 09:25:51 2021
Thread-2: Wed May 26 09:25:53 2021
Thread-1: Wed May 26 09:25:53 2021
Thread-1: Wed May 26 09:25:55 2021
Thread-1: Wed May 26 09:25:57 2021
Thread-2: Wed May 26 09:25:59 2021
Thread-2: Wed May 26 09:26:05 2021
Thread-2: Wed May 26 09:26:11 2021
Thread-2: Wed May 26 09:26:17 2021

 

例2:4个线程分别执行loop函数,中间等待nsec秒,nsec分别是4,2,3,5

from time import ctime
from time import sleep
import _thread

loops = [4,2,3,5]

def loop(nloop,nsec,lock):   #nloop表示的是标识,nsec表示的是睡眠时间,lock表示的是锁对象
    print('start loop',nloop,'at:',ctime())
    sleep(nsec)
    print('done loop',nloop,'at:',ctime())
    lock.release()                        #释放锁

def main():
    print('start at:',ctime())
    locks=[]
    nloops=range(len(loops))             #nloops=range(4)
    for i in nloops:
           lock=_thread.allocate_lock()  #分配锁对象
           lock.acquire()                #获取锁对象
           locks.append(lock)
    print(locks)
    #启动一个线程
    for i in nloops:
        _thread.start_new_thread(loop,(i,loops[i],locks[i]))    #启动一个新线程

    #等待所有的锁被释放
    for i in nloops:
        while(locks[i].locked()):
            pass
    print('all done at:', ctime())

if __name__ == '__main__':
    main()


##输出结果:
start at: Wed May 26 09:31:06 2021
[<locked _thread.lock object at 0x000001FC4951C750>, <locked _thread.lock object at 0x000001FC49583A20>, <locked _thread.lock object at 0x000001FC49583AB0>, <locked _thread.lock object at 0x000001FC49583C60>]
start loopstart loop 3start loop start loop 0 2  at:  at:1Wed May 26 09:31:06 2021
 at: Wed May 26 09:31:06 2021 Wed May 26 09:31:06 2021

at: Wed May 26 09:31:06 2021
done loop 1 at: Wed May 26 09:31:08 2021
done loop 2 at: Wed May 26 09:31:09 2021
done loop 0 at: Wed May 26 09:31:10 2021
done loop 3 at: Wed May 26 09:31:11 2021
all done at: Wed May 26 09:31:11 2021

Process finished with exit code 0

程序运行的的总时间是5s。main()函数最后一个循环的作用是等待所有子线程退出。

 

8.threading模块:

threading模块不仅提供了Thread类,还提供了各种非常好用的同步机制。_thread模块不支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求,就一直等着

如果设定一个线程为守护线程,就表示这个线程不重要,在进程退出时,不用等待这个线程退出。如果主线程退出时不用等待子线程完成,就需要设定这些线程的daemon属性,即在线程Thread.start()开始前,调用setDaemon()函数设定线程的daemon标志(Thread.setDaemon(True)),就表示这个线程不重要。

如果一定要等子线程执行完在退出主线程,什么都不用做或者显示调用(Thread.setDaemon(False))以保证daemon标志是false,可以调用Thread.isDaemon()函数判断daemon标志的值。新的子线程会继承父线程的daemon标志,整个python在所有非守护线程退出后才会结束,即进程中没有非守护线程存在时才结束。

 

threading模块除了包含_thread模块中的所有方法,还包含其他方法:

  • threading.currentThread()   #返回当前的线程变量
  • threading.enumerate()       #返回一个包含正在运行的线程列表,正在运行指的是线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount()     #返回正在运行的线程数量,与len(threading.enumerate)有相同的结果

除了上面讲的使用方法外,线程模块提供了Thread类来处理线程,Thread类提供了以下方法:

  • run()                                #表示线程活动的方法
  • start()                             #启动线程活动
  • join([time])                     #等待至线程终止。这阻塞调用线程直至线程的join()方法被调用终止-正常退出或者抛出未处理的异常-或者是可选的超时发生
  • isAlive()                        #返回线程是否活动的
  • getName()                   #返回线程名
  • setName()                  #设置线程名

 

例3,使用threading模块创建线程
可以直接从threading.Thread继承创建一个新的子类,并实例化后调用start()方法启动新的线程,即它调用了线程的run()方法

import threading
import time
exitFlag=0

class myThread(threading.Thread):
    def __init__(self,threadID,name,counter):
        threading.Thread.__init__(self)
        self.threadID=threadID
        self.name =name
        self.counter=counter
    def run(self):
        print('开始线程:' + self.name)
        print_time(self.name,self.counter,5)
        print('退出线程:' + self.name)

def print_time(threadName,delay,counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print('%s: %s' % (threadName,time.ctime(time.time())))
        counter -=1

#创建新线程
thread1=myThread(1,'Thread-1',1)
thread2=myThread(2,'Thread-2',2)

#启动新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print('退出主线程')

##输出结果:
开始线程:Thread-1
开始线程:Thread-2
Thread-1: Wed May 26 09:41:39 2021
Thread-2: Wed May 26 09:41:40 2021
Thread-1: Wed May 26 09:41:40 2021
Thread-1: Wed May 26 09:41:41 2021
Thread-2: Wed May 26 09:41:42 2021
Thread-1: Wed May 26 09:41:42 2021
Thread-1: Wed May 26 09:41:43 2021
退出线程:Thread-1
Thread-2: Wed May 26 09:41:44 2021
Thread-2: Wed May 26 09:41:46 2021
Thread-2: Wed May 26 09:41:48 2021
退出线程:Thread-2
退出主线程

 

9.线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。

 

考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。

锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。 经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。

 

例4:

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开启线程: " + self.name)
        # 获取锁,用于线程同步
        threadLock.acquire()
        print_time(self.name, self.counter, 3)
        # 释放锁,开启下一个线程
        threadLock.release()

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

threadLock = threading.Lock()
threads = []

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()

# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")


##输出结果:
开启线程: Thread-1
开启线程: Thread-2
Thread-1: Wed May 26 09:50:03 2021
Thread-1: Wed May 26 09:50:04 2021
Thread-1: Wed May 26 09:50:05 2021
Thread-2: Wed May 26 09:50:07 2021
Thread-2: Wed May 26 09:50:09 2021
Thread-2: Wed May 26 09:50:11 2021
退出主线程

 

10. 线程优先级队列( Queue)

Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。

这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。

Queue 模块中的常用方法:

  • Queue.qsize()                                返回队列的大小
  • Queue.empty()                              如果队列为空,返回True,反之False
  • Queue.full()                                   如果队列满了,返回True,反之False
  • Queue.full                                     与 maxsize 大小对应
  • Queue.get([block[, timeout]])       获取队列,timeout等待时间
  • Queue.get_nowait()                    相当Queue.get(False)
  • Queue.put(item)                         写入队列,timeout等待时间
  • Queue.put_nowait(item)             相当Queue.put(item, False)
  • Queue.task_done()                    在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
  • Queue.join()                               实际上意味着等到队列为空,再执行别的操作

 

例5:

import queue
import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, q):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.q = q
    def run(self):
        print ("开启线程:" + self.name)
        process_data(self.name, self.q)
        print ("退出线程:" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s processing %s" % (threadName, data))
        else:
            queueLock.release()
        time.sleep(1)

threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = queue.Queue(10)
threads = []
threadID = 1

# 创建新线程
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# 填充队列
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# 等待队列清空
while not workQueue.empty():
    pass

# 通知线程是时候退出
exitFlag = 1

# 等待所有线程完成
for t in threads:
    t.join()
print ("退出主线程")


##输出结果:
开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-3 processing Two
Thread-2 processing Three
Thread-1 processing Four
Thread-3 processing Five
退出线程:Thread-2
退出线程:Thread-1
退出线程:Thread-3
退出主线程

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值