Python学习笔记十二(多任务、线程、互斥锁)

多任务

什么是多任务

生活中,你可能一边听歌,一边写作业;一边抱着孩子,一边打着电话;一边干活,一边聊天。。。这些都是生活中的多任务场景。电脑上的多任务,一边运行音乐程序,一边用Google Chrome写笔记;一边用Google Chrome写着笔记,一边用Google Chrome查资料。。。。这些事实电脑上的多任务。

从上面可以简单归纳一下,多任务就是同一时间内做多件事情 或者 同一时间内运行多个程序。电脑上是由CPU执行多任务的,CPU从单核到现在的多核都能实现多任务,为什么单核CPU也能实现多任务?这就不得不说操作系统了,分时操作系统[1]是使一台计算机采用时间片轮转的方式同时为几个、几十个甚至几百个用户服务的一种操作系统,通过时间片的轮转CPU切换任务,因为CPU运行的足够快,所以感觉是多个任务同时执行。

并行与并发[2]

当CPU的核心数小于要执行的任务时,也就是所谓的并发[3]
当CPU的核心数大于或等于要执行的任务时,也就是所谓的并行[4]
简单理解,并行就是一段时间内我可以边看电视,边聊天。并发就像是使用电脑,一会敲敲键盘一会点点鼠标。


线程

CPU执行多任务,实际上是CPU 调度对应的线程去工作,也可以说线程是CPU 调度的基本单位。

什么是线程

可以理解成执行代码的分支,线程是执行对应的代码的。那么线程怎么完成多任务?

单线程

模拟人的使用电脑


 import time


# 敲键盘ing
def keyboard():
    for i in range(5):
        print("敲键盘ing...")
        time.sleep(0.5)


# 点鼠标ing
def mouse():
    for i in range(5):
        print("点鼠标ing...")
        time.sleep(1)


if __name__ == '__main__':
    keyboard()
    mouse()

    # 运行结果:
    # 敲键盘ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # 点鼠标ing...
    # 点鼠标ing...
    # 点鼠标ing...
    # 点鼠标ing...


这是一个单线程单任务,不能实现我们的需求。有没有其他方式?

多线程

既然一个线程只能运行一个任务,我们有两个线程,所以应该至少有两个线程,才能满足我们的需求。

Python 中开启多线程可以使用threading 模块。


import threading
import time


# 敲键盘ing
def keyboard():
    for i in range(5):
        print("敲键盘ing...")
        time.sleep(0.5)


# 点鼠标ing
def mouse():
    for i in range(5):
        print("点鼠标ing...")
        time.sleep(1)


if __name__ == '__main__':
    # 为了方便,我们使用两个线程分别执行 keyboard和 mouse任务
    print("开辟前,当前有%s个线程在活动,他们是%s" %
          (threading.active_count(), threading.enumerate()))

    keyboard_thread = threading.Thread(target=keyboard)
    mouse_thread = threading.Thread(target=mouse)

    print("开辟后,当前有%s个线程在活动,他们是%s" %
          (threading.active_count(), threading.enumerate()))

    keyboard_thread.start()
    mouse_thread.start()

    print("start后,当前有%s个线程在活动,他们是%s" %
          (threading.active_count(), threading.enumerate()))

    time.sleep(2)
    # 使用完毕
    print("play over")
    
    # 运行结果:
    # 开辟前,当前有1个线程在活动, 他们是[ < _MainThread(MainThread, started
    # 140005808121600) >]
    # 开辟后,当前有1个线程在活动, 他们是[ < _MainThread(MainThread, started
    # 140005808121600) >]
    # 敲键盘ing...
    # start后,当前有3个线程在活动, 他们是[ < _MainThread(MainThread, started
    # 140005808121600) >, < Thread(Thread - 1, started
    # 140005782394624) >, < Thread(Thread - 2, started
    # 140005774001920) >]
    # 点鼠标ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # play
    # over
    # 敲键盘ing...
    # 点鼠标ing...
    # 点鼠标ing...


分析结果

  • 可以发现开辟前就有一个线程,为什么呢?因为我们写的代码要执行,而线程就是执行代码的,所以开辟线程前就有一条线程符合逻辑,不然谁去执行我们的代码呢?这条默认的线程有另一个称呼叫做主线程。
  • threading.Thread() 只是准备创建一条子线程,并没有创建(开辟后,当前有1个线程在活动)
  • bathe_thread.start() 的时候,才创建了子线程并运行起来(start后,当前有3个线程在活动)
  • 线程之间执行是无序(输出结果无序,当然效果不太明显,你可以多循环几次,或者多运行几次看看结果是不是一样)
  • 主线程结束后,子线程并未结束;子线程结束后,主线程结束,说明主线程默认会等待子线程执行结束。(start后,当前有3个线程在活动是主程序的最后一行,主程序运行在主线程上)

有一个问题,我们都是用完电脑了,怎么还有输出?或者说怎么让子线程跟随主线程一起结束?

设置守护线程


import threading
import time


# 敲键盘ing
def keyboard():
    for i in range(5):
        print("敲键盘ing...")
        time.sleep(0.5)


# 点鼠标ing
def mouse():
    for i in range(5):
        print("点鼠标ing...")
        time.sleep(1)


if __name__ == '__main__':
    keyboard_thread = threading.Thread(target=keyboard)
    mouse_thread = threading.Thread(target=mouse)

    # 设置守护,注意必须在start之前执行
    keyboard_thread.setDaemon(True)
    mouse_thread.daemon = True

    keyboard_thread.start()
    mouse_thread.start()

    time.sleep(2)
    # 使用完毕
    print("play over")

    # 运行结果:
    # 敲键盘ing...
    # 点鼠标ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # play over
 

设置守护线程有两种方式,通过thread 对象的setDaemon() 方法,或者thread 对象的daemon 属性设置子线程守护主线程,当主线程结束,子线程被销毁。

小结:

  • 获取活动线程的数量
  • len(threading.enumerate())
  • threading.active_count()
  • 获取执行当前代码的线程对象
  • threading.current_thread()
  • 线程之间的执行是无序的
  • 默认情况下,主线程会等待所有子线程都执行完,程序才结束
  • 设置守护线程( setDaemon() daemon ),主线程结束,子线程被销毁
  • 注意点:主线程里的子线程如果有一个没有设置守护,那么主线程会等待这个子线程执行结束,因为主线程没有结束,所以守护线程不会被销毁。
  • 注意点:设置守护线程( setDaemon() daemon ),要在start() 之前设置,否则会报错(RuntimeError: cannot set daemon status of active thread)。

自定义线程

手动设置守护线程稍微有些麻烦,万一忘了设置守护就得不到预期效果,怎么让得到的线程就是一个守护线程?自定义线程,设置守护。


import threading
import time


class DaemonThread(threading.Thread):
    """自定义线程 - 默认守护"""

    def __init__(
            self, target=None, name=None, args=(), kwargs=None, ):

        # 调用父类初始化,设置daemon 为True
        super(DaemonThread, self).__init__(
            target=target, name=name, args=args, kwargs=kwargs, daemon=True)


# 敲键盘ing
def keyboard():
    for i in range(5):
        print("敲键盘ing...")
        time.sleep(0.5)


# 点鼠标ing
def mouse():
    for i in range(5):
        print("点鼠标ing...")
        time.sleep(1)


if __name__ == '__main__':
    keyboard_thread = threading.Thread(target=keyboard)
    mouse_thread = threading.Thread(target=mouse)

    # 设置守护,注意必须在start之前执行
    keyboard_thread.setDaemon(True)
    mouse_thread.daemon = True

    keyboard_thread.start()
    mouse_thread.start()

    time.sleep(2)
    # 使用完毕
    print("play over")

    # 运行结果:
    # 敲键盘ing...
    # 点鼠标ing...
    # 敲键盘ing...
    # 点鼠标ing...
    # 敲键盘ing...
    # 敲键盘ing...
    # play over



从结果上看通过自定义我们得到了想要的效果。那么有一个问题,什么问题呢?问题就是什么是自定义线程?
有人说自定义线程必须重写父类的run 方法,有人说不需要(就是我),怎么理解呢?

我是这么认为的,首先怎么自定义一个类,或者说怎么定义一个类,class 类名(父类名): 在实现一些逻辑就成为了一个类,一个自己定义的类,那么必须要重写__init__()方法 或者 __new__()方法吗?显然是看需求,类比一下,自定义一个线程必须要重写run() 方法吗?我的run() 方法不需要自己取实现什么逻辑,为什么要去重写呢?

上面是类比,下面反推一下,根据结果可以看出DaemonThread()具有线程的功能(那是必须的DaemonThread 类是Thread类的子类),所以DaemonThread 是一个自定义线程类。

问最简单的自定义线程是什么?根据上面的理解可以得出,继承threading.Tread 然后 pass(空实现)这就是一个最简单的自定义线程。

多线程与全局变量

我想看看娱乐了多少次,做一个记录


import threading
import time

# 娱乐次数
amusement_counter = 0


# 吃饭ing
def eat():
    global amusement_counter
    for i in range(5):
        # print("吃饭ing...")

        amusement_counter += 1
        time.sleep(0.1)


# 唱歌ing
def sing():
    global amusement_counter
    for i in range(5):
        # print("唱歌ing...")

        amusement_counter += 1
        time.sleep(0.2)


# 洗澡ing
def bath():
    for i in range(5):
        global amusement_counter
        # print("洗澡ing...")

        amusement_counter += 1
        time.sleep(0.3)


if __name__ == '__main__':
    # 为每个任务开辟一个线程
    eat_thread = threading.Thread(target=eat)
    sing_thread = threading.Thread(target=sing)
    bath_thread = threading.Thread(target=bath)

    # 启动线程
    eat_thread.start()
    sing_thread.start()
    bath_thread.start()

    time.sleep(2)
    print(amusement_counter)

# 结果为
# 15


可以看出线程之间共享全局变量,共享处处有啊,共享单车遍地开花,但是共享单车的问题也不少,那么共享的全局变量会不会有问题呢?往下看


# 使用多线程对全局变量进行累加,为了更方便的体现问题,所以每个任务对全局变量累加1000000次
import threading

counter = 0


def func():
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
    print("func ", counter)


def func1():
    global counter
    for i in range(1000000): # 100,0000
        counter += 1
    print("func1 ", counter)


if __name__ == '__main__':
    func_thread = threading.Thread(target=func)
    func1_thread = threading.Thread(target=func1)

    # 启动线程
    func_thread.start()
    func1_thread.start()

# 结果为:
# func  1476347
# func1  1577818


从结果看出和我们想要的结果不一样啊,对全局变量总共累加了200,0000次 ,但是得到的结果却小于这个数,为什么呢?先看一个小游戏,使用python实现一下。

有一辆车,两个人去抢,谁抢到则胜利场数加一,同时抢到则两人的胜利场数不变。


import threading
import random

# 两个人的胜利场数
winA = 0
winB = 0

# 游戏的回合数
counter = 0

if __name__ == '__main__':

    for i in range(10):
        counter += 1

        # 使用随机数,为1则记为胜利,胜利数加1
        flagA = random.randint(0, 1)
        flagB = random.randint(0, 1)

        if flagA == flagB:
            continue

        if flagA:
            winA += 1

        if flagB:
            winB += 1
    print("游戏总场数%d" % counter)
    print("A胜利%d场" % winA)
    print("B胜利%d场" % winB)

# 结果为:
# 游戏总场数10
# A胜利3场
# B胜利4场


分析结果,游戏总共有10个回合,A的胜场+B的胜场 等于 7个回合,比游戏回合少。是不是感觉有些熟悉?上面我们对全局变量总共累加了200,0000次得到了一个小于200,0000次的数据。
类比一下,全局变量就是我们的车,两个子线程A线程与B线程就相当于A与B,他们同时去取值得到的是同一个值,比如同时取到100,那么A线程操作完全局变量是101,B线程操作完也是101,所以两次累加操作,只有一次是有效的。也就是所谓的资源竞争问题,怎么解决资源竞争?排队,让A线程先去执行,执行完了,B线程再去执行,


import threading

import time

counter = 0


def func():
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
    print("func ", counter)


def func1():
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
    print("func1 ", counter)


if __name__ == '__main__':
    func_thread = threading.Thread(target=func)
    func1_thread = threading.Thread(target=func1)

    # 启动线程
    func_thread.start()
    
    # 获取当前时间
    join_start = time.time()
    
    # 主线程等待子线程执行结束,在继续执行
    func_thread.join()
    
    # 计算时间差
    print(time.time() - join_start)
    func1_thread.start()
    
    # 运行结果:
    # func 1000000
    # 0.153519868850708  # 相差0.15,也就是说程序在join 的时候睡眠的大概0.15秒
    # func1 2000000


通过线程协同(join() 方法),虽然完成了我们的需求,但是也让我们的多任务变成了单人任务,有没有其他方式呢?

互斥锁

通过加锁的方式也可以解决资源竞争问题。


import threading

import time

lock = threading.Lock()  # 得到一把锁
counter = 0


def func():
    lock.acquire()  # 加锁

    global counter
    for i in range(1000000):  # 100,0000
        counter += 1

    print("func ", counter)

    lock.release()  # 解锁


def func1():
    lock.acquire()
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
    print("func1 ", counter)
    lock.release()


if __name__ == '__main__':
    func_thread = threading.Thread(target=func)
    func1_thread = threading.Thread(target=func1)

    # 启动线程
    func_thread.start()
    func1_thread.start()
    
    # 运行结果:
    # func 1000000
    # func1 2000000


互斥锁也可以解决资源竞争的问题,但是也让我们的多任务变为了单任务。

验证同步,互斥锁让多任务变为单任务

验证同步


import threading 
counter = 0


def func():
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
        if i % 100000 == 0:
            print("func ")

    print("func ", counter)


def func1():
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
        if i % 100000 == 0:
            print("func1 ")
    print("func1 ", counter)


if __name__ == '__main__':
    func_thread = threading.Thread(target=func)
    func1_thread = threading.Thread(target=func1)

    # 启动线程
    func_thread.start()
    func_thread.join()
    func1_thread.start()
    
# 运行结果:
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func  1000000
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1  2000000
# 通过结果可以看出,先执行了func,执行完了之后,又执行的func1 


验证锁


import threading

import time

lock = threading.Lock()  # 得到一把锁
counter = 0


def func():
    lock.acquire()  # 加锁

    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
        if i % 100000 == 0:
            print("func ")
    print("func ", counter)

    lock.release()  # 解锁


def func1():
    lock.acquire()
    global counter
    for i in range(1000000):  # 100,0000
        counter += 1
        if i % 100000 == 0:
            print("func1 ")
    print("func1 ", counter)
    lock.release()


if __name__ == '__main__':
    func_thread = threading.Thread(target=func)
    func1_thread = threading.Thread(target=func1)

    # 启动线程
    func_thread.start()
    func1_thread.start()
 

# 运行结果:
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func 
# func  1000000
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1 
# func1  2000000
# 通过结果可以看出,先执行了func,执行完了之后,又执行的func1 

小结:

  • 多线程共享全局变量,但共享全局变量会产生资源竞争问题
  • 线程同步(join())和互斥锁都能解决资源竞争问题,但是都是将多任务变成了单任务。
  • 使用锁要避免死锁
  • 注意点:如果两个子线程共享全局变量,只对一个子线程加锁,可以理解为和没加锁一样。
  • 注意点:效率问题,同样是累加200,0000次,直接遍历累加200,0000次所用时间小于线程同步所用时间小于互斥锁所用时间

到此结   DragonFangQy   2018.4.26
欢迎指正
  感谢!


  1. 分时操作系统 ↩

  2. 并行与并发 ↩

  3. 并发 ↩

  4. 并行 ↩

你可能感兴趣的:(Python学习笔记十二(多任务、线程、互斥锁))