多线程

本文深入探讨了多线程的概念,包括线程与进程的区别、Python中的`threading`模块以及如何实现线程。通过实例展示了线程的创建、数据共享、互斥锁的使用以解决非线程安全问题,同时提到了死锁问题和同步应用,如生产者消费者模式。文章还讨论了线程中的变量和线程与进程的区别,揭示了多线程在效率和稳定性方面的权衡。
摘要由CSDN通过智能技术生成

1. 线程

        线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。什么进程呢?进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程和进程的区别和联系:

1、一个程序中至少有一个进程,一个进程中至少有一个线程;

2、线程的划分尺度小于进程(占有资源),使得多线程程序的并发性高;

3、进程运行过程中拥有独立的内存空间,而线程之间共享内存,从而极大的提高了程序的运行效率

4、线程不能独立运行,必须存在于进程中

二者之间优缺点:线程开销小,但是不利于资源的管理和保护,而进程反之。

2. threading模块实现线程

Python2中支持多线程编程的模块有两个thread、threading,但是官方已经不建议使用thread模块。

Python3中取消了thread模块,只有threading模块,所以我们使用threading模块来学习多线程编程。

1.一个简单的单线程
def say():
    i = 0
    while i<3:
        print("Today I am very happy")
        i += 1


t1 = threading.Thread(target=say)

t1.start()

运行结果:

Today I am very happy
Today I am very happy
Today I am very happy

其实下面也是一个单线程

def say():                
    print("Today I am vary
    time.sleep(1)         
                          
                          
if __name__ == '__main__':
    for i in range(5):    
        say()             
    print("End of code")  
2.一个简单的多线程
from threading import Thread, current_thread


def say():
    print("Today I am vary happy")
    # current_thread().name 获得当前进程的名字
    print("The name of thread is ==>", current_thread().name)


if __name__ == '__main__':
    print("当前线程的名称是 ==>", current_thread().name)
    for x in range(5):
        t = Thread(target=say, name="线程%s" % x)
        t.start()
    print("代码结束")

运行结果如下:

当前线程的名称是 ==> MainThread
Today I am vary happy
The name of thread is ==> 线程0
Today I am vary happy
The name of thread is ==> 线程1
Today I am vary happy
The name of thread is ==> 线程2
Today I am vary happy
The name of thread is ==> 线程3
Today I am vary happy
代码结束
The name of thread is ==> 线程4

如果没有join或者设置为守护线程,主线程会直接执行后面的代码,之后挂起,等待所有的子线程运行完成后结束,才能结束.\

3.多进程方法带参数
import threading, time                                                           
                                                                                 
                                                                                 
def say(msg):                                                                    
    print("今天我很开心 ~ ~ ~ ", msg)                                                  
    # time.sleep(1)                                                              
    print("当前线程的名字是 ~ ~ ~ ", threading.current_thread().name)                    
                                                                                 
                                                                                 
if __name__ == '__main__':                                                       
    print("当前线程的名字是 ~ ~ ~ ", threading.current_thread().name)                    
    t1 = threading.Thread(target=say, args=("这是一个参数Tiny",), name="线程东方红")        
    # t1.daemon = True                                                           
    t1.start()                                                                   
    t2 = threading.Thread(target=say, args=["这个是参数Tom"], name="线程神州行")           
    # t2.daemon = True                                                           
    t2.start()                                                                   
                                                                                 
    count = threading.enumerate()                                                
    print(count)                                                                 
    print(type(count))                                                           
    print("当前线程的数量是{}".format(len(count)))                                       

运行结果:

当前线程的名字是 ~ ~ ~  MainThread
今天我很开心 ~ ~ ~  这是一个参数Tiny
今天我很开心 ~ ~ ~  这个是参数Tom
[<_MainThread(MainThread, started 4540)>, <Thread(线程东方红, started daemon 2364)>, <Thread(线程神州行, started daemon 1292)>]
<class 'list'>
当前线程的数量是3

不加time.sleep(1)时进程的数量为1或2。由于线程的运行速度是很快的,而且是并发的,当线程t1和线程t2都运行完后,主线程才运行完,这是线程的数目就只有一个了,当t1和t2两个子线程有一个落后于主线程时,此时线程的数目有2个。

4. 线程类的实现

要想用类实现多线程,要首先继承threading的Thread方法,然后重写run函数。下面是一个简单的线程类的实现

import threading                         
                                         
                                         
class MyThread(threading.Thread):        
    def run(self):                       
        for i in range(5):               
            print("正在下载中")               
                                         
                                         
if __name__ == '__main__':               
    # t1 = MyThread().run()              
    t1 = MyThread()                      
    print(t1.name)                       
    t1.start()                           
                                         

运行结果如下

Thread-1
正在下载中
正在下载中
正在下载中
正在下载中
正在下载中
5.线程类的实现,带参数
import threading                                               
                                                               
                                                               
class MyThread(threading.Thread):                              
                                                               
    def __init__(self, name, path):                            
        super().__init__(name=name)                            
        self.path = path                                       
                                                               
    def run(self):                                             
        for i in range(3):                                     
            print("正在下载中……", self.path)                        
                                                               
                                                               
if __name__ == '__main__':                                     
    t1 = MyThread("天王盖地虎", "C://a/b/log.log")                  
    print(t1.name)                                             
    t1.start()                                                 

运行结果如下

天王盖地虎
正在下载中…… C://a/b/log.log
正在下载中…… C://a/b/log.log
正在下载中…… C://a/b/log.log

3.多线程的数据共享

1.多线程中的全局变量
from threading import Thread
num = 10


def run1():
    global num
    for i in range(5):
        num += 1
    print(num)


def run2():
    global num
    for i in range(5):
        num += 1
    print(num)


if __name__ == '__main__':
    t1 = Thread(target=run1)
    t2 = Thread(target=run2)
    t1.start()
    t2.start()

运行结果如下

15
20

由此可知,多线程中的全局变量是共享的

2.多线程中的数据安全问题

由于是多线程访问,有可能出现下⾯情况:在num=10时,t1取得num=10。此时系统把t1调度为”sleeping”状态,把t2转换
为”running”状态,t2也获得num=10。然后t2对得到的值进⾏加1并赋给num,使得num=11。然后系统⼜把t2调度为”sleeping”,把t1转为”running”。线程t1⼜把它之前得到的10加1后赋值给num。这样,明明t1和t2都完成了1次加1⼯

作,但结果仍然是num=11。下面来看一个简单的程序

from threading import Thread
num = 0


def run1():
    global num
    for i in range(1000000):
        num += 1
    print(num)


def run2():
    global num
    for i in range(1000000):
        num += 1
    print(num)


if __name__ == '__main__':
    t1 = Thread(target=run1)
    t2 = Thread(target=run2)
    t1.start()
    t2.start()

运行结果是

1148404
1193515

问题产⽣的原因就是没有控制多个线程对同⼀资源的访问,对数据造成破坏,使得线程运⾏的结果不可预期。这种现象称为“非线程安全”。

3. 互斥锁----非线程安全问题的解决

        在引入互斥锁之前,先引入同步的概念。同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。"同"字是指协同、协助、互相配合。如进程、线程同步,可理解为进程或线程A和B⼀块配合,A执⾏到⼀定程度时要依靠B的某个结果,于是停下来,示意B运⾏。B依⾔执⾏,再将结果给A,A再继续操作。     

        提出的那个计算错误的问题,可以通过线程同步来进⾏解决,如:系统调⽤t1,然后获取到num的值为0,此时上⼀把锁,即不允许其他现在操作num对num的值进⾏+1解锁,此时num的值为1,其他的线程就可以使⽤num了,⽽且是num的值不是0⽽是1同理其他线程在对num进⾏修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。

        当多个线程⼏乎同时修改某⼀个共享数据的时候,需要进⾏同步控制线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁。互斥锁为资源引⼊⼀个状态:锁定/⾮锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有⼀个线程进⾏写⼊操作,从⽽保证了多线程情况下数据的正确性,hreading模块中定义了Lock类,可以⽅便的处理锁定。

下面看一个程序

from threading import Thread, Lock
num = 0
my_lock = Lock()# 申请一个全局锁


def run1():
    global num
    for i in range(1000000):
        my_lock.acquire()# 对操作的代码进行加锁
        num += 1
        my_lock.release()# 对操作的代码进行解锁
    print(num)


def run2():
    global num
    for i in range(1000000):
        my_lock.acquire()
        num += 1
        my_lock.release()
    print(num)


if __name__ == '__main__':
    t1 = Thread(target=run1)
    t2 = Thread(target=run2)
    t1.start()
    t2.start()

运行结果:

1984687
2000000

锁的好处:确保了某段关键代码只能由⼀个线程从头到尾完整地执⾏

锁的坏处:阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

4.多线程中的非全局变量

在多线程开发中,全局变量是多个线程都共享的数据,⽽局部变量等是各⾃线程的,是⾮共享的。所以不存在非线程安全问题

下面看一个程序

from threading import Thread


def run1():
    b = 10
    for i in range(10):
        b += 1
    print(b)


def run2():
    b = 10
    for i in range(10):
        b += 1
    print(b)


t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()

运行结果如下

20
20

4.死锁问题

        在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源,就会造成死锁。尽管死锁很少发⽣,但⼀旦发⽣就会造成应⽤的停⽌响应。

下面是一个死锁

from threading import Thread, Lock

my_lock1 = Lock()
my_lock2 = Lock()
num = 0


def run1():
    global num
    for i in range(100):
        if my_lock1.acquire():
            if my_lock2.acquire():
                num += 1
                my_lock2.release()
            my_lock1.release()
    print(num)


def run2():
    global num
    for i in range(100):
        if my_lock2.acquire():
            if my_lock1.acquire():
                num += 1
                my_lock1.release()
            my_lock2.release()
    print(num)


t1 = Thread(target=run1)
t2 = Thread(target=run2)
t1.start()
t2.start()

运行时无结果,被卡住。

我们在设计代码的时候,一定要注意避免死锁的出现。可以使用如下的方案:

(1). 银行家算法              (2). 添加超时时间等(不建议使用,加锁部分可能不会执行)

5.同步应用

多个线程有序执行的一个案例
from threading import Thread, Lock

m1 = Lock()
m2 = Lock()
m3 = Lock()

m2.acquire()
m3.acquire()


def task1():
    while True:
        if m1.acquire():
            print("任务一开始运行了……")
            m2.release()


def task2():
    while True:
        if m2.acquire():
            print("任务二开始运行了……")
            m3.release()


def task3():
    while True:
        if m3.acquire():
            print("任务三开始运行了……")
            m1.release()


t1 = Thread(target=task1)
t2 = Thread(target=task2)
t3 = Thread(target=task3)
t1.start()
t2.start()
t3.start()

运行结果:

任务一开始运行了……
任务二开始运行了……
任务三开始运行了……
任务一开始运行了……
任务二开始运行了……
任务三开始运行了……
任务一开始运行了……
任务二开始运行了……
任务三开始运行了……
……

6.生产者模式和消费者模式

        在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产⽣数据的模块,就形象地称为生产者;⽽而处理数据的模块,就称为消费者。

        单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。

Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先⼊先出)队列Queue,LIFO(后⼊先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原⼦操作,即要么不做,要么就做完),能够在多线程中直接使⽤。可以使⽤队列来实现线程间的同步。

下面是一个生产者和消费者模式

q = queue.Queue(5)

foots = ["馒头", "辣条", "面条", "油条", "大饼", "豆浆", "包子", "窝窝头"]


def make_foot():
    while True:
        temp = foots[r.randint(0, 7)]
        q.put(temp)
        print("%s制作了%s食物" % (current_thread().name, temp))
        print("当前食物还有%s个" % (q.qsize()))
        # time.sleep(1)


def eat_foot():
    while True:
        temp = q.get()
        print("%s吃了%s食物" % (current_thread().name, temp))
        print("当前剩余%s个食物" % (q.qsize()))
        # time.sleep(0.5)


t1 = Thread(target=make_foot, name="中华神厨小当家")
t2 = Thread(target=eat_foot, name="西游取经猪无能")
t1.start()
t2.start()

运行结果如下

中华神厨小当家制作了辣条食物
当前食物还有1个
中华神厨小当家制作了油条食物
当前食物还有2个
中华神厨小当家制作了包子食物
当前食物还有3个
中华神厨小当家制作了大饼食物
当前食物还有4个
中华神厨小当家制作了窝窝头食物
西游取经猪无能吃了辣条食物
当前剩余4个食物
西游取经猪无能吃了油条食物
当前剩余3个食物
西游取经猪无能吃了包子食物
当前剩余2个食物
西游取经猪无能吃了大饼食物
当前剩余1个食物
西游取经猪无能吃了窝窝头食物
当前剩余0个食物
当前食物还有0个
中华神厨小当家制作了豆浆食物
当前食物还有1个
中华神厨小当家制作了窝窝头食物
当前食物还有2个
中华神厨小当家制作了包子食物
西游取经猪无能吃了豆浆食物
当前剩余2个食物
西游取经猪无能吃了窝窝头食物
当前食物还有1个
中华神厨小当家制作了油条食物
当前食物还有2个
当前剩余2个食物
西游取经猪无能吃了包子食物
当前剩余1个食物
西游取经猪无能吃了油条食物
当前剩余0个食物
中华神厨小当家制作了豆浆食物
当前食物还有1个
中华神厨小当家制作了面条食物
当前食物还有2个
中华神厨小当家制作了油条食物
当前食物还有3个
中华神厨小当家制作了包子食物
当前食物还有4个
中华神厨小当家制作了馒头食物
当前食物还有5个
西游取经猪无能吃了豆浆食物
当前剩余4个食物
西游取经猪无能吃了面条食物
当前剩余3个食物
西游取经猪无能吃了油条食物
当前剩余2个食物
西游取经猪无能吃了包子食物
当前剩余1个食物
西游取经猪无能吃了馒头食物
当前剩余0个食物
……

        在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么⽣生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

        生产者消费者模式是通过⼀个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以生产者⽣产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了生产者和消费者的处理能⼒。 这个阻塞队列就是⽤来给生产者和消费者解耦的。纵观大多数设计模式,都会找⼀个第三者出来进行解耦

7.线程中的变量

下面看两个程序:

def print_msg(name, age, nickname):
    print("开始打印信息")
    speak(name)
    eat(age, nickname)


def speak(name):
    print("你叫====》", name)


def eat(age,nickname):
    print("要吃饭,先说出你的名字和年纪", age, nickname)

from threading import Thread
if __name__ == '__main__':
    t1 = Thread(target=print_msg, args=["老王", 36, "隔壁的"])
    t2 = Thread(target=print_msg, args=["小美", 28, "夜店的"])
    t1.start()
    t2.start()
from threading import Thread, local
threadLocal = local()


def print_msg(name, age, nickname):
    print("开始打印信息")
    threadLocal.name = name
    threadLocal.age = age
    threadLocal .nickname = nickname
    speak()
    eat()


def speak():
    print("你叫====》", threadLocal.name)


def eat():
    print("要吃饭,先说出你的名字和年纪", threadLocal.age, threadLocal.nickname)


if __name__ == '__main__':
    t1 = Thread(target=print_msg, args=["老王", 36, "隔壁的"])
    t2 = Thread(target=print_msg, args=["小美", 28, "夜店的"])
    t1.start()
    t2.start()

这两个程序的运行结果都是:

开始打印信息
你叫====》 老王
要吃饭,先说出你的名字和年纪 36 隔壁的
开始打印信息
你叫====》 小美
要吃饭,先说出你的名字和年纪 28 夜店的

8.线程和进程

        多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。

        多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

        多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。

        在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值