Python多线程编程初步探索(一)

Python多线程编程初步探索(一)

多线程的概念

参考博文:https://www.jianshu.com/p/e50b9e4ce5aa
什么是进程?什么是线程?
“操作系统进行资源分配的最小单元是进程,即进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序,数据集,进程控制块三部分组成.我们编写的程序 用来描述进程完成那些功能以及如何完成;数据集 则是程序在执行过程中多需要使用的资源;进程控制块 用来记录进程外部特征,描述进程的执行变化过程,系统可以用它来控制和管理进程,它是系统感知进程存在的唯一标志。”——简单来说,进程就是一次程序的执行,在执行中会用到数据以及其他的一些资源。是资源分配的最小单元。
“线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元”。“操作系统进行运算调度的最小单元是线程”。——简单来说,线程是CPU一次运算调度。是运算的最小单元。
综上,进程的概念要大于线程。对于一个应用程序或软件,它开始执行,则系统为其开启一个进程(也可能是多个,暂不讨论)。系统默认开启一个主线程,如果没有创建多线程,则该主线程沿时间线(对应程序代码从上到下)开始调度运算,直至主线程结束,程序才能退出。故此,主线程相当于一个程序的“后台主线”,主线程贯穿程序的整个周期。

多个线程

  1. 为什么要创建多个线程? 假设一段程序代码,拟实现“A+B+C”的功能,A为听音乐,B为写博客,C为打印,他们所消耗的时间分别为7、8、9秒,则程序总体消耗时间为7+8+9秒,为一种串行的结构(对应代码自上而下的运行)。像一条单车道,先跑A段,再跑B段C段。
    如何提速呢?可以将A、B、C设置为3个线程,同时运行,这样最终程序的时间为9秒。像三车道,A、B、C同时跑。为一种并行的结构(实为“并发”,并行与并发暂不讨论)。
  2. 如何在Python中创建多个线程? 采用threading模块中的Thread类直接创建或继承式创建。子线程是在默认的主线程(“后台主线”)上创建出来的。
    (1)Thread类直接创建
# 多线程的并发,只能是交给一个cpu执行,不能多个cpu执行.即多个线程不能实现并行.

# 多线程并发方式一:
import threading
import time
def tingge():
    print("听歌")
    time.sleep(3)
    print("听歌结束时间:")
    print(time.time() - s)# 计算整个此线程运行时间
def xieboke():
    print("写博客")
    time.sleep(5)
    print("写博客结束时间:")
    print(time.time()-s)  # 计算整个此线程运行时间

s = time.time()
t1 = threading.Thread(target=tingge)  # 创建听歌线程,多线程的子线程.
t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
t1.start()  # 运行听歌线程,多线程的子线程
t2.start()  #运行写博客线程,多线程的子线程
time.sleep(0)
print("主线程‘前端’结束时间:")  # 该print在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
=========================
听歌
写博客
主线程‘前端’结束时间:
0.01013493537902832

听歌结束时间:
3.018101930618286
写博客结束时间:
5.015822887420654

该三线程,最长的线程为写博客,消耗5s,故总的程序耗时(即后台主线程耗时)为5s。可以反复调整各time.sleep()中线程的耗时时间,观测程序结果。
在该实例中,听歌、写博客、主线程‘前端’三个线程为并列关系,谁也不等谁,谁最后结束,程序才结束。
如果采用传统的单线程,则程序总耗时为8秒。代码为:

# 不采用多线程并发。即传统的串行结果仿真。
import threading
import time
def tingge():
    print("听歌")
    time.sleep(3)
    print("听歌结束时间:")
    print(time.time()-s)# 计算整个此线程运行时间
def xieboke():
    print("写博客")
    time.sleep(5)
    print("写博客结束时间:")
    print(time.time()-s)  # 计算整个此线程运行时间

s = time.time()
# t1 = threading.Thread(target=tingge)  # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start()  # 运行听歌线程,多线程的子线程
# t2.start()  #运行写博客线程,多线程的子线程
tingge()
xieboke()
time.sleep(0)
print("主线程‘前端’结束时间:")  # ‘前端’在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
# 总的程序耗时为听歌耗时+写博客耗时+主线程耗时。

(2) Thread类继承式创建:

# 调用多线程方式二
import threading
import time

class MyThread(threading.Thread):
     def __init__(self,num):
         threading.Thread.__init__(self)
         self.num = num

     def run(self):
         print("running on  number:%s" %self.num)
         time.sleep(3)

t1 = MyThread(56)
t2 = MyThread(78)
t1.start()   # 该进程运行run函数原因,请查看源码,一系列的调用最终是调用run函数
t2.start()   # 该进程运行run函数原因,请查看源码,一系列的调用最终是调用run函数
print("ending")
  1. 创建多个线程可能会遇到哪些问题?(不影响对其他问题的理解时,不再区分主线程‘前端’或后台)
    (1)当各线程之间相互独立时:各线程及主线程是一个简单的“并”。如以上实例所示。
    (2)当各线程之间不独立时:
    (2a)如果主线必须要等待某一个子线程结束后方能结束,则可以采用join()的方法。join()使得子线程加入到主线程中,即在子线程完成之前,主线程(或称父线程、父进程)将一直被阻塞。
    如:
import threading
from time import ctime,sleep
import time
def Music(name):
   print("Begin listenning to {name}.{time}".format(name=name,time=ctime()))
   sleep(3)
   print("end listening {time}".format(time=ctime()))

def Blog(title):
   print("Begin recording the {title}.{time}".format(title=title,time=ctime()))
   sleep(5)
   print("end recording {time}".format(time=ctime()))

threads = []

t1 = threading.Thread(target=Music,args=("FILL ME",))
t2 = threading.Thread(target=Blog,args=("My Blog",))

threads.append(t1)
threads.append(t2)

if __name__ == "__main__":
     for t in threads:
         t.start()

     t2.join()
     print("Main thread ‘head’ over %s "%ctime())


=========================
Begin listenning to FILL ME.Sat Nov  2 17:49:41 2019
Begin recording the My Blog.Sat Nov  2 17:49:41 2019
end listening Sat Nov  2 17:49:44 2019
end recording Sat Nov  2 17:49:46 2019
Main thread  ‘head’ over Sat Nov  2 17:49:46 2019

此时,写博客堵塞了主线程。相当于两车道,听歌一个车道,写博客和主线程在一个车道。
如果将t2.join()改为t1.join(),则结果为:

Begin listenning to FILL ME.Sat Nov  2 17:52:31 2019
Begin recording the My Blog.Sat Nov  2 17:52:31 2019
end listening Sat Nov  2 17:52:34 2019
Main thread ‘head’ over Sat Nov  2 17:52:34 2019 
end recording Sat Nov  2 17:52:36 2019

此时,听歌堵塞了主线程,即听歌和主线程在一个车道,写博客在一个车道。
如果当t.join()在for循环内就不能实现多线程了,没有意义。

if __name__ == "__main__":
      for t in threads:
          t.start()
          t.join()
=======================
Begin listenning to FILL ME.Sat Nov  2 17:54:58 2019
end listening Sat Nov  2 17:55:01 2019
Begin recording the My Blog.Sat Nov  2 17:55:01 2019
end recording Sat Nov  2 17:55:06 2019
Main thread ‘head’ over Sat Nov  2 17:55:06 2019

此时,听歌先堵塞主线程,然后写博客再堵塞主线程,即三者在一个车道,多线程无意义了。
(2b)在以上实例中,主线程‘前端’与其并行的子线程互不干扰,即便主线程‘前端’结束了,后台主线程还是要等到所有的子线程都结束后,程序才退出。有的时候,我们希望主线程‘前端’结束的时候,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon()方法了。
setDaemon()设置守护线程:将某一线程声明为守护线程(必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起(应该是所有线程结束后就退出了)),这样当主线程可以结束时,守护线程也就一起退出了。如:

# 主线程可以结束但子线程未结束,整个程序同样结束。
import threading
from time import ctime, sleep


def Music(name):
    print("Begin listening to {name}. {time}".format(name=name, time=ctime()))
    sleep(3)
    print("end listening {time}".format(time=ctime()))


def Blog(title):
    print("Begin recording the {title}. {time}".format(title=title, time=ctime()))
    sleep(5)
    print('end recording {time}'.format(time=ctime()))


threads = []

t1 = threading.Thread(target=Music, args=('FILL ME',))
t2 = threading.Thread(target=Blog, args=('python',))

threads.append(t1)
threads.append(t2)

if __name__ == '__main__':
    for t in threads:
        t.setDaemon(True)  # 注意:一定在start之前设置
        t.start()
    sleep(2)
    print("all over %s" % ctime())
==============================================
Begin listening to FILL ME. Sat Nov  2 19:08:44 2019
Begin recording the python. Sat Nov  2 19:08:44 2019
all over Sat Nov  2 19:08:46 2019

此时,t1,t2均为守护线程(即都是主线程的守护神),主线程在2s后要结束时,其所有守护神也一并退出了。
如果仅将t1设置为守护线程,结果为:

    t1.setDaemon(True)  # 注意:一定在start之前设置
    t1.start()
    t2.start()
    sleep(2)
    print("all over %s ?" % ctime())
========================================
Begin listening to FILL ME. Sat Nov  2 19:16:38 2019
Begin recording the python. Sat Nov  2 19:16:38 2019
all over Sat Nov  2 19:16:40 2019 ?
end listening Sat Nov  2 19:16:41 2019
end recording Sat Nov  2 19:16:43 2019

此时,仅t1为守护线程,当2s后主线程准备结束时,由于t2独立运行(耗时最长),后台主线程不能退出,等到t2完成时,所有其他线程也运行完毕。
如果仅将t2设置为守护线程,结果为:

Begin listening to FILL ME. Sat Nov  2 20:16:30 2019
Begin recording the python. Sat Nov  2 20:16:30 2019
all over Sat Nov  2 20:16:32 2019 ?
end listening Sat Nov  2 20:16:33 2019

此时,仅t2为守护线程,当2s后主线程准备结束时,由于t1独立运行(耗时3s),后台主线程不能退出,等到t1完成时,后台主线程退出。整个程序随之结束。
4. 其它方法

Thread实例对象的方法
t.start() : 激活线程,
t.getName() : 获取线程的名称
t.setName() : 设置线程的名称
t.name : 获取或设置线程的名称
t.is_alive() : 判断线程是否为激活状态
t.isAlive() :判断线程是否为激活状态
t.setDaemon() 设置为后台线程或前台线程(默认:False);通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之后才可以使用。如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
t.isDaemon() : 判断是否为守护线程
t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
t.join() :逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
t.run():线程被cpu调度后自动执行线程对象的run方法
threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
5. 关于线程的退出
没有特别的设置时,线程运算完毕即自退出。下面的实例可以直观的说明:

# -*- coding: utf-8 -*-
"""
Created on Fri Oct 25 09:46:38 2019

@author: Maguoxin
"""

import threading
import time

exitFlag = 0# 没有用到
 
class myThread (threading.Thread):   #继承父类threading.Thread
    def __init__(self, threadID, name, counter, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
        self.delay = delay
    def run(self):                   #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数。当run运行完毕时即自退出
        print ("Starting " + self.name)
        print_time(self.name, self.counter, self.delay)# 每个线程都要调用这个函数。可以同时调用。
        print(threading.enumerate())
        print ("Exiting " + self.name)
 
def print_time(threadName, counter, delay):
    while counter:# 当线程调用此函数时,此函数内含有一个循环。当循环counter次后,循环即结束跳出。此函数亦即执行完毕。
        # if exitFlag:
        #     (threading.Thread).exit() # 没有用到
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1
 
# 创建新线程
thread1 = myThread(1, "Thread-1", 8,3)# 线程1调用print_time函数时,函数内循环体共循环8次,每次delay时间为3s
thread2 = myThread(2, "Thread-2", 10,2)# 线程2调用print_time函数时,函数内循环体共循环10次,每次delay时间为2s
 
# 开启线程
thread1.start()
thread2.start()

time.sleep(25)
print(threading.enumerate())
print ("Exiting Main Thread")
===================================
Starting Thread-1
Starting Thread-2
Thread-2: Tue Nov  5 21:00:31 2019
Thread-1: Tue Nov  5 21:00:32 2019
Thread-2: Tue Nov  5 21:00:33 2019
Thread-1: Tue Nov  5 21:00:35 2019
Thread-2: Tue Nov  5 21:00:35 2019
Thread-2: Tue Nov  5 21:00:37 2019
Thread-1: Tue Nov  5 21:00:38 2019
Thread-2: Tue Nov  5 21:00:39 2019
Thread-1: Tue Nov  5 21:00:41 2019
Thread-2: Tue Nov  5 21:00:41 2019
Thread-2: Tue Nov  5 21:00:43 2019
Thread-1: Tue Nov  5 21:00:44 2019
Thread-2: Tue Nov  5 21:00:45 2019
Thread-1: Tue Nov  5 21:00:47 2019
Thread-2: Tue Nov  5 21:00:47 2019
Thread-2: Tue Nov  5 21:00:49 2019
[<_MainThread(MainThread, started 17140)>, <myThread(Thread-1, started 5400)>, <myThread(Thread-2, started 16740)>]
Exiting Thread-2#此时线程2是真的退出了,由下可见
Thread-1: Tue Nov  5 21:00:50 2019
Thread-1: Tue Nov  5 21:00:53 2019
[<_MainThread(MainThread, started 17140)>, <myThread(Thread-1, started 5400)>]#由此可见
Exiting Thread-1#此时线程1也自退出了
[<_MainThread(MainThread, started 17140)>]
Exiting Main Thread
  1. 发现的那些坑儿
    (1)当采用threading.Thread(target=tingge)直接创建线程时,发现了一个问题,当tingge后有小()时,就不能创建新线程,而是在主线程中开始调用tingge函数。当上述实例中tingge和xieboke后仅添加两个小()后,结果没有实现多线程,而是传统的主线程自己运行的结果:
t1 = threading.Thread(target=tingge())  # 不能创建多线程的子线程,而是主线程开始执行tingge的功能.
t2 = threading.Thread(target=xieboke()) # 亦不能创建多线程的子线程,而是主线程开始执行写博客的功能.
===================================
听歌
听歌结束时间:
3.0002450942993164
写博客
写博客结束时间:
8.000455856323242
主线程‘前端’结束时间:
8.002456903457642

即便是在threading.Thread(添加name=‘ 起名 ’),也不能改变这个情况。这里个人认为是threading.Thread创建新线程的一个大的bug。请反复实现下列代码进行试验,即可观测结果。
但是使用类继承的方式创建,则可以在tingge后添加小(),并没有bug出现。而我们最终添加这个小()的目的,是为了调用函数传递实参。

# 当threading.Thread直接创建多线程时,发现:如果target=后的function()则不能实现多线程

# 多线程实验
import threading
import time
def tingge():
    print("听歌")
    time.sleep(3)
    print("听歌结束时间:")
    print(time.time()-s)# 计算整个此线程运行时间
    print(threading.enumerate())
def xieboke():
    print("写博客")
    time.sleep(5)
    print("写博客结束时间:")
    print(time.time()-s)  # 计算整个此线程运行时间
    print(threading.enumerate())

s = time.time()
#''' 实验1
# t1 = threading.Thread(target=tingge)  # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start()  # 运行听歌线程,多线程的子线程
# t2.start()  #运行写博客线程,多线程的子线程
# time.sleep(0)
#''' 实验1

#''' 实验2
# t1 = threading.Thread(target=tingge)  # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(target=xieboke) # 创建写博客线程,多线程的子线程
# t1.start()  # 运行听歌线程,多线程的子线程
# t2.start()  #运行写博客线程,多线程的子线程
# time.sleep(8.1)
#''' 实验2

#''' 实验3
# t1 = threading.Thread(target=tingge())  # 当此处tingge有()时,发现并没有创建新线程。而是主线程在运行tingge函数功能
# t2 = threading.Thread(target=xieboke()) # 同上,主线程在运行xieboke函数功能
# t1.start()  # 并没有新的线程启动
# t2.start()  # 并没有新的线程启动
# time.sleep(8.1)
#''' 实验3

#''' 实验4
# t1 = threading.Thread(name='a',target=tingge())  # 添加线程名称,不能改观结果
# t2 = threading.Thread(name='b',target=xieboke()) #
# t1.start()  # 并没有新的线程启动
# t2.start()  # 并没有新的线程启动
# time.sleep(8.1)
# #''' 实验4

#''' 实验5
# t1 = threading.Thread(name='a',target=tingge)  # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(name='b',target=xieboke()) # 主线程在运行xieboke函数功能
# t1.start()  # tingg作为一个新线程
# t2.start()  # 并没有新的线程启动
# time.sleep(8.1)
#''' 实验5

#''' 实验6
# t1 = threading.Thread(name='a',target=tingge())  # 创建听歌线程,多线程的子线程.
# t2 = threading.Thread(name='b',target=tingge()) # 主线程在运行xieboke函数功能
# t1.start()  # 并没有新的线程启动
# t2.start()  # 并没有新的线程启动
# time.sleep(3.1)
#''' 实验6

#''' 实验7
# class MyThread(threading.Thread):
#     def __init__(self):
#         threading.Thread.__init__(self)
#
#     def run(self):
#         tingge()
#
#
# t1 = MyThread()
# t2 = MyThread()
# t1.start()  # 有新的线程启动
# t2.start()  # 有新的线程启动
# time.sleep(3.1)
#''' 实验7

#''' 实验8
def tingge_new(delay):
    print("听歌")
    time.sleep(delay)
    print("听歌结束时间:")
    print(time.time()-s)# 计算整个此线程运行时间
    print(threading.enumerate())

class MyThread(threading.Thread):
    def __init__(self, name, delay):
        threading.Thread.__init__(self)
        self.name = name
        self.delay = delay

    def run(self):
        tingge_new(self.delay)


t1 = MyThread("Thread-1", 3)
t2 = MyThread("Thread-2", 5)
t1.start()  # 有新的线程启动
t2.start()  # 有新的线程启动
time.sleep(3.1)
#''' 实验8

print("主线程‘前端’结束时间:")  # ‘前端’在多线程的主线程上
print(time.time()-s)# 计算‘前端’运行时间
print(threading.enumerate())
#

注:早期的Python2.0 thread模块,后变更为_thread,中的_thread.start_new_thread(函数名,(实参1,))的方式可以启用新线程并向函数传递实参,但该模块其他问题较多,不建议使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值