Pythoner集中营Python学习

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

2018-04-26  本文已影响64人  DragonFangQy

多任务

什么是多任务

生活中,你可能一边听歌,一边写作业;一边抱着孩子,一边打着电话;一边干活,一边聊天。。。这些都是生活中的多任务场景。电脑上的多任务,一边运行音乐程序,一边用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...


分析结果

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

设置守护线程


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 属性设置子线程守护主线程,当主线程结束,子线程被销毁。

小结:

自定义线程

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


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 

小结:


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


  1. 分时操作系统

  2. 并行与并发

  3. 并发

  4. 并行

上一篇下一篇

猜你喜欢

热点阅读