Python 初识线程

一. 背景知识

1. 进程

在已经了解了操作系统中进程的概念后,我们对进程有了一定的了解: 程序是不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程. 程序和进程的区别就在于: 程序是指令的集合, 它是进程运行的静态描述文本; 进程是程序的一次执行活动,属于动态概念. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行. 这样的设计,大大他搞了CPU的利用率. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的.

2. 有了进程为何还要线程?

(1)什么是线程?线程指的是流水线式的工作过程,一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,而是资源单位,分配资源的单位.线程才是执行单位.

(2)进程与线程的对比:

  a.同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源是隔离的.

  b.创建线程对资源的消耗远远小于创建进程的消耗

(3)进程有很多优点,它提供了多道编程,提高了计算机的利用率,让每个人感觉自己独享着CPU和其他资源.然而,进程也是有缺点的,主要体现在两点上:

  a.进程只能在同一时间执行一个任务,如果想要同时执行两个或多个任务,进程就无能为力了.

  b.进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行.

3. 线程的出现

60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建,撤销与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程运行开销过大.

因此,在80年代,出现了能独立运行的基本单位--线程(Threads).

需要注意的是: 进程是资源分配的最小单位,线程是CPU调度的最小单位.每个进程中至少有一个线程.

在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程.我们可以把一个车间的工作过程看作是一个进程,把一条流水线工作的过程看作是一个线程.车间不仅要负责把资源整合到一起,而且一个车间内至少要有一条流水线.

所以总结来说: 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位.

多线程的概念是: 在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间.

二. 进程和线程的关系

线程与进程的区别可归纳为以下4点:

1. 地址空间和其他资源(如打开文件): 进程间相互独立,同一进程的各线程间共享. 某进程内的线程在其他进程不可见.

2. 通信: 进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信----需要进程同步和互斥手段的辅助,以保证数据的一致性(类似于进程中的锁的作用).

3. 调度和切换: 线程上下文切换比进程上下文切换要快得多.

4. 在多线程操作系统中,进程不是一个可执行的实体,真正去执行程序的不是进程,而是线程.可以理解为进程就是一个线程的容器.

三. 线程的特点

在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体. 线程具有以下属性:

1. 轻型实体

线程中的实体基本上不拥有系统资源,只是有一些必不可少的,能保证独立运行的资源.

线程的实体包括程序,数据和TCB.线程是动态概念,它的动态特性有线程控制块TCB(Thread Control Block)描述.

#TCB包括以下信息:
(1)线程状态.
(2)当线程不运行时,被保存的现场资源.
(3)一组执行堆栈.
(4)存放每个线程的局部变量主存区.
(5)访问同一个进程中的主存和其它资源.
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈.

2. 独立调度和分派的基本单位

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位.由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的线程).

3. 共享进程资源

线程在同一进程中的各个线程, 都可以共享该进程所拥有的资源, 这首先表现在: 所有线程都具有相同的进程id, 这意味着, 线程可以访问该进程的每一个内存资源; 此外, 还可以访问进程所拥有的已打开文件、定时器、信号量机构等. 由于同一个进程内的线程共享内存和文件, 所以线程之间互相通信不必调用内核.

4. 可并发执行

在一个进程中的多个线程之间, 可以并发执行, 甚至允许在一个进程中所有线程都能并发执行; 同样, 不同进程中的线程也能并发执行, 充分利用和发挥了处理机与外围设备并行工作的能力.
 
 
 
四. Python与线程
待续
 
 
 
五. Threading模块
multiprocess模块完全模仿了threading模块的接口,二者在使用层面有很大的相似性.
1. 线程创建的两种方式
import time
from threading import Thread    # 引入线程模块

def func(n):    # 自定义一个函数
    time.sleep(1)
    print(n)

if __name__ == '__main__':
    t = Thread(target=func, args=("hello,world",))  # 创建线程对象
    t.start()   # 开启线程
    print("主线程结束")
创建线程方式一
import time
from threading import Thread    # 引入线程模块

class MyThread(Thread): # 自定义一个类
    def __init__(self, n):  # 传参n
        super().__init__()  # 自己想要传参,必须先super()执行父类的init方法,再写自己的实例变量
        self.n = n
    def run(self):  # 自定义一个run()方法,内容不定,但是run名称不能变
        time.sleep(1)
        print(self.n)

if __name__ == '__main__':
    t = MyThread("hello,world")  # 创建线程对象
    t.start()   # 开启线程
    print("主线程结束")
创建线程方式二

 2. 进程与线程开启效率比较

import time
from threading import Thread
from multiprocessing import Process

def func(n):
    sum = 0
    for i in range(n):
        sum += i

if __name__ == '__main__':
    t_start_time = time.time()  #开始时间
    t_list = []
    for i in range(10):
        t = Thread(target=func, args=(100,))
        t.start()
        t_list.append(t)
    [tt.join() for tt in t_list]
    t_end_time = time.time()    #结束时间
    t_dif_time = t_end_time - t_start_time  #时间差

    p_start_time = time.time()  #开始时间
    p_list = []
    for ii in range(10):
        p = Process(target=func, args=(100,))
        p.start()
        p_list.append(p)
    [pp.join() for pp in p_list]
    p_end_time = time.time()    #结束时间
    p_dif_time = p_end_time - p_start_time  #时间差

    print("线程的执行时间是>>>", t_dif_time)
    print("进程的执行时间是>>>", p_dif_time)
    print("主线程结束")

# 执行结果:
# 线程的执行时间是>>> 0.0010013580322265625
# 进程的执行时间是>>> 0.37227368354797363
# 主线程结束
进程与线程开启效率比较

从上面代码的结果中可以看出: 执行同一个任务,线程的执行时间远远小于进程的执行时间.因此,线程的效率是比较高的.

3. 同一进程下线程是资源共享的

from threading import Thread

num = 100
def func():
    global num
    num = 0

if __name__ == '__main__':
    t = Thread(target=func,)
    t.start()
    t.join()
    print(num)

# 执行结果:
# 0
同一进程下线程是资源共享的

4. 线程共享数据时,数据是不安全的

import time
from threading import Thread

num = 100   #全局变量
def func():
    global num
    # 模拟num-=1的"取值->计算->赋值"过程
    mid = num       #取值
    mid = mid - 1   #计算
    time.sleep(0.0001)
    num = mid       #赋值

if __name__ == '__main__':
    t_list = []
    for i in range(10): #创建10个子线程
        t = Thread(target=func,)
        t.start()
        t_list.append(t)
    [tt.join() for tt in t_list]    # 主线程等待子线程执行结束
    print('主线程结束,此时全局变量为>>>', num)

# 执行结果:
# 主线程结束,此时全局变量为>>> 99
演示共享资源的时候,数据不安全的问题
import time
from threading import Thread, Lock

num = 100   # 全局变量
def func(t_lock):
    global num
    t_lock.acquire()    # 加锁
    # 模拟num-=1的"取值->计算->赋值"过程
    mid = num       # 取值
    mid = mid - 1   # 计算
    time.sleep(0.001)
    num = mid       # 赋值
    t_lock.release()    # 解锁

if __name__ == '__main__':
    t_lock = Lock() # 创建同步锁(互斥锁)对象
    t_list = []
    for i in range(10): # 创建10个子线程
        t = Thread(target=func, args=(t_lock,))
        t.start()
        t_list.append(t)
    [t.join() for t in t_list]  # 主线程等待子线程执行结束
    print('主线程结束,此时全局变量为>>>', num)
    
# 执行结果:
# 主线程结束,此时全局变量为>>> 90
通过引入线程模块里的锁来解决数据不安全的问题

5. 守护线程

无论是进程还是线程,都遵循: 守护进程(线程)会等待主进程(线程)运行完毕后被销毁. 需要强调的是: 运行完毕并非终止运行.

#1. 对于主进程来说,运行完毕指的是主进程代码运行完毕
#2. 对于主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程全部执行完毕,主线程才算运行完毕.

详细解释:

#1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束.
#2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收). 因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束,因为进程执行结束是要回收资源的.

观察以下两个例子,对比两个例子中函数的睡眠时和执行结果:

import time
from threading import Thread

def func1(n):
    time.sleep(4)
    print(n)

def func2(n):
    time.sleep(2)
    print(n)

if __name__ == '__main__':
    t1 = Thread(target=func1, args=("我是子线程1号",))
    t1.daemon = True    #设置守护线程
    t1.start()
    t2 = Thread(target=func2, args=("我是子线程2号",))
    t2.start()
    print("主线程结束")

# 执行结果:
# 主线程结束
# 我是子线程2号
例1:守护线程func1睡眠4秒,非守护线程func2睡眠2秒
import time
from threading import Thread

def func1(n):
    time.sleep(1)
    print(n)

def func2(n):
    time.sleep(2)
    print(n)

if __name__ == '__main__':
    t1 = Thread(target=func1, args=("我是子线程1号",))
    t1.deamon = True    # 设置守护线程,必须放到start前面
    t1.start()
    t2 = Thread(target=func2, args=("我是子线程2号",))
    t2.start()
    print("主线程结束")

# 执行结果:
# 主线程结束
# 我是子线程1号
# 我是子线程2号
例2:守护线程func1睡眠1秒,非守护线程func2睡眠2秒

对比两个例子的执行结果,可以看出,主线程等待所有非守护线程的结束才结束.当主线程的代码运行结束后,还要等待非守护线程执行完毕,在这个等待的过程中,守护线程并没有消亡,还在继续执行.

对比守护进程与守护线程:

  守护进程: 主进程的代码执行完毕后,整个程序并没有结束,且主进程仍然存在着,因为主进程要等待其它子进程执行完毕,回收子进程的残余资源(为子进程收尸).总之,主进程的代码执行完毕后守护进程也跟着结束----守护进程随着主进程的消亡而消亡.

  守护线程: 主线程的代码执行完毕后,整个程序并没有结束,且主线程仍然存在着,因为主线程要等待所有非守护线程执行完毕,随后,当所有线程全部执行完毕后,主线程结束,这也意味着主进程的结束,最后主进程回收所有资源.总之,主线程的代码执行完毕后要等待非守护线程执行完毕,在这个等待过程中,守护进程没有消亡,直到等待结束,随主线程的消亡而消亡.

六. 信号量

同进程的一样, Semaphore管理一个内置的计数器:
每当调用acquire()时内置计数器-1; 
调用release()时内置计数器+1;
计数器不能小于0;当计数器为0时, acquire()将阻塞线程直到其他线程调用release().
from threading import Thread,Semaphore
import threading
import time

def func():
    if sm.acquire():    # 加锁
        print(threading.currentThread().getName() + " get semaphore")
        time.sleep(3)
        sm.release()    # 解锁

if __name__ == '__main__':
    sm = Semaphore(5)   # 创建信号量对象,限制锁内每次只能进入5个线程
    for i in range(25): # 创建25个线程
        t = Thread(target=func,)
        t.start()
例1
from threading import Thread,Semaphore
import threading
import time

def func():
    sm.acquire()    # 加锁
    print('%s get semaphore' % threading.current_thread().getName())
    time.sleep(3)
    sm.release()    # 解锁

if __name__ == '__main__':
    sm = Semaphore(5)   # 创建信号量对象,限制锁内每次只能进入5个线程
    for i in range(25): # 创建25个线程
        t = Thread(target=func,)
        t.start()
例2
 
总结:
  信号量: 控制同时能够进入锁内去执行代码的线程数量 (进程数量 ), 维护了一个计数器 刚开始创建信号量的时候假如设置的是 5个房间(sm = Semaphore(5)) , 一个房间每次只能进一个线程. 一个线程进入一次 acquire那么sm就减 1, 出来一次sm就 +1, 如果计数器为 0, 那么acquire()将阻塞住, 其他的线程就需要等待 这样其他的线程和正在执行的这一组(5个)线程就是一个同步的状态 而进入 acquire里面去执行的那 5个线程则是异步的 .

七. 锁

 待续

猜你喜欢

转载自www.cnblogs.com/haitaoli/p/9849992.html