[并发编程 - 多线程:线程相关概念、开启线程的两种方式、线程对象方法、守护线程、互斥锁]

[并发编程 - 多线程:线程相关概念、开启线程的两种方式、线程对象方法、守护线程、互斥锁]

线程相关概念

  • 什么是线程?

  • 线程:一个流水线的运行过程

    进程内代码的运行过程

    进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),
    线程线程是一个执行单位,cpu执行的就是线程

    多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控

    制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

线程的创建开销小

  • 创建进程的开销远大于线程

    创建子进程要开一个内存空间,把父进程数据拷一份给子进程,该空间里至少要有一条流水线,这样开销很大

    创建子线程资源不用申请,就用当前进程就可以,只是提交的时候有一段代码要并发运行,创建线程的开销要远远小于进程,几乎是这个请求发出的同时,这个线程立马就创建好了

  • 进程之间是竞争关系,线程之间是协作关系?

进程与线程的区别

1.线程共享创建它的进程的地址空间;进程有自己的地址空间

2.线程可以直接访问其进程的数据段;进程有自己的父进程的数据段副本

3.线程可以直接与其进程中的其他线程通信;进程必须使用进程间通信来与同级进程通信

4.新线程很容易创建;新的进程需要父进程的复制。

5.线程可以对同一进程的线程进行相当大的控制;进程只能对子进程进行控制。

6.主线程的改变(取消,优先级的改变等)可能会影响进程中其他线程的行为;父进程的更改不会影响子进程

  • 总结:

    1、同一进程下的多个线程共享该进程的内存资源

    2、开启子线程的开销要远远小于开启子进程

  • 为何要用多线程

  • 多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

    1. 多线程共享一个进程的地址空间
    2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
    3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许些活动彼此重叠运行,从而会加快程序执行的速度。
    4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

开启线程的两种方式

threading模块介绍

开启线程的threading模块提供了一个比thread模块更高层的API来提供线程的并发性。这些线程并发运行并共享内存。

 Thread类的使用 :目标函数可以实例化一个Thread对象,每个Thread对象代表着一个线程,可以通过start()方法,开始运行。

开启线程的方式一

每创建一个进程,默认有一个线程,就是主线程。进程要想执行,要先创建一个主线程,然后由这个进程内的线程去运行代码

# 1.证明开启子线程的开销小
from threading import Thread,current_thread
import os

def task():
    # current_thread()会得到当前线程的线程对象.name能获取它的名字
    print("%s is running" %current_thread().name)

if __name__ == '__main__':  # Windows规定开启线程的代码必须放在它下面
    t = Thread(target=task)
    t.start()  # 通知操作系统开启子线程 
    print('主线程',current_thread().name) #.name默认名,可以更改.name=名字


# 2.证明同一进程下的多个线程共享该进程的内存资源
from threading import Thread, current_thread

n = 100

def task():
    global n   # 声明n是全局变量
    n = 0

if __name__ == '__main__':
    t = Thread(target=task)
    t.start()  # 因为线程的运行太快了,有可能看到100
    t.join()  # 加join等线程运行完,打印永远看不到100的值
    print("主线程",n)

开启线程的方式二

自己写一个子类去继承它

from threading import Thread

class Mythread(Thread): # 自定义的类必须继承Thread类
    # 重写了init方法 父类就被覆盖掉
    def __init__(self, name):
        super().__init__() # 重用父类 因为父类还有很多有用功能,继承父类
        self.name = name

    def run(self) -> None:  # 方法一定要写run
        print("%s is running" % self.name)

if __name__ == '__main__':
    # 实例化直接用自己自定义的类开子线程
    t = Mythread("线程1")  # args 为函数传参数 没有就不传
    t.start()  # 通知系统开启子线程

线程对象相关的方法

# Thread实例对象的方法
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。

# threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
    
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、
    			       结束前,不包括启动前和终止后的线程。
    
threading.activeCount(): 返回正在运行的线程数量,len(threading.enumerate())
    					 有相同的结果。
        
group:线程组,目前还没有 实现

target: 要执行的方法,就是要执行的程序
         是将被run()方法调用的可调用对象。默认为None,表示不调用任何东西
    
name:线程的名字,默认情况下以Thread-N的形式构造一个唯一的名字,N是一个小的十进制整数

args:给调用程序传入的值()

kwargs:给程序传入的值,默认为{}

# Thread实例对象的方法
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。

# threading模块提供的一些方法:
threading.currentThread(): 返回当前的线程变量。
    
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、
    			       结束前,不包括启动前和终止后的线程。
    
threading.activeCount(): 返回正在运行的线程数量,len(threading.enumerate())
    					 有相同的结果。
        
group:线程组,目前还没有 实现

target: 要执行的方法,就是要执行的程序
         是将被run()方法调用的可调用对象。默认为None,表示不调用任何东西
    
name:线程的名字,默认情况下以Thread-N的形式构造一个唯一的名字,N是一个小的十进制整数

args:给调用程序传入的值()

kwargs:给程序传入的值,默认为{}

守护线程

守护进程是守护主进程的代码

守护线程是守护主线程的生命周期

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

1.对主进程来说,运行完毕指的是主进程代码运行完毕

2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,
  主线程才算运行完毕

详细解释

复制1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直
   等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束
   意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread,current_thread,active_count,enumerate
import os
import time

def task(n):
    print("%s is running" %current_thread().name)
    time.sleep(n)
    print("%s is end" %current_thread().name)


if __name__ == '__main__':
    t1 = Thread(target=task,args=(3,))
    t2 = Thread(target=task,args=(5,))
    t3 = Thread(target=task,args=(100,))
    t3.daemon = True  # 守护线程,守护主线程的生命周期

    t1.start()
    t2.start()
    t3.start()
    print("主") # 主线程5秒钟结束
    
"""
# 打印结果
Thread-1 is running
Thread-2 is running
Thread-3 is running
主
Thread-1 is end
Thread-2 is end

过程分析:
t1、t2、t3三个线程在背后都开始运行了,t1、t2都是正常的子线程,
t3则是守护线程,守护着主线程的生命周期 t3按正常来说是100秒结束,
但主线程在5秒后就结束了,一旦结束就直接带走t3了,主线程应该很快就
运行完了,但是要在原地等t1、t2运行完主进程才能结束,也就5秒钟,可
t3还没有运行完还是被一起带走了,所以无法看到Thread-3 is end

"""

互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

互斥锁是一种简单的加锁的方法来控制对共享资源的访问,对共享数据进行锁定,保证同一时刻只能有一个线程去操作。

【互斥锁的特点】:

  1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;

  2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

  3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

【互斥锁的操作流程如下】:

  1. 在访问共享资源后临界区域前,对互斥锁进行加锁;

  2. 在访问完成后释放互斥锁导上的锁。在访问完成后释放互斥锁导上的锁;

  3. 对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

from threading import Thread
import time

n = 100
def task():
    global n
    temp = n
    time.sleep(0.1)  # 设置延迟
    n = temp - 1


if __name__ == "__main__":
    n = 100
    thread_l = []
    for i in range(100):
        t=Thread(target=task)
        l.append(p)
        t.start()
    for obj in thread_l:
        obj.join()

    print("主",n,time.time()-start_time)
    
'''
代码运行完成结果大概率为99,小概率为98,运行时间则是略大于0.1秒。并发执行,速度快,数据不安全:
'''    
    

需要注意:
join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,
要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高


from threading import Thread,Lock
import os
import time

n = 100
mutex = Lock()

def task():
    global n
    with mutex:
        temp = n
        time.sleep(0.1)
        n = temp - 1

if __name__ == '__main__':
    thread_l = []
    start_time = time.time()
    for i in range(100):
        t = Thread(target=task)
        thread_l.append(t)
        t.start()

    for obj in thread_l:
        obj.join()

    print("主",n,time.time()-start_time)
    
'''
代码运行结果则一定是0,但是这样也有一个缺点就是运行速度降低
了运行时间在10秒多一点,但是为了数据安全这些时间是必须的!

'''
    
    

提示:加上互斥锁,哪个线程抢到这个锁我们决定不了,哪个先抢到锁的那么就是哪个线程先执行,没有抢到的线程需要等待

加上互斥锁多任务瞬间变成单任务,性能会下降,也就是说同一时刻只能有一个线程去执行

posted @ 2021-04-24 12:46  刘较瘦丫  阅读(57)  评论(0编辑  收藏  举报