python并发编程之threading线程(一)

进程是系统进行资源分配最小单元,线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.进程在执行过程中拥有独立的内存单元,而多个线程共享内存等资源。

系列文章

threading模块创建线程

import threading
from threading import Thread

def test(x):
    print('this is {}'.format(x))
    time.sleep(2)

def get_thread(number=5):
    l_thread = (Thread(target=test, args=(i,)) for i in range(number))
    for t in l_thread:
        print(t)
        t.start() # 启动线程开始执行
    print(len(threading.enumerate()))
if __name__ == '__main__':
    get_thread(5)

# 结果
<Thread(Thread-1, initial)>
this is 0
<Thread(Thread-2, initial)>
this is 1
<Thread(Thread-3, initial)>
this is 2
<Thread(Thread-4, initial)>
this is 3
<Thread(Thread-5, initial)>
this is 4
6

通过以上可知,我们只需要创建一个Thread对象,并运行start方法,解释器就会创建一个子进程执行我们的target,我们创建了5个线程,但是使用threading.enumerate查看线程的数量发现有6个线程,因为当前在执行的还有一个主线程。主线程会默认等待所有的子线程结束后再结束。

  • 我们还有另外一种创建线程的方式
import threading
from threading import Thread
class MyThread(Thread):

    def __init__(self, x):
        super().__init__()
        self.x = x

    def run(self):
        print('this is {}'.format(self.x))
        time.sleep(2)

def get_thread1(number=5):
    l_thread = (MyThread(i) for i in range(number))
    for t in l_thread:
        print(t.name)
        t.start()
    print(len(threading.enumerate()))

if __name__ == '__main__':
    get_thread1(5)

Thread对象有一个run方法,它就是我们需要执行的目标函数,所以我们可以通过继承Thread对象,重写run方法,将我们的目标代码放置在run方法中。

Thread对象分析

class Thread:
    def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):
        pass

# Thread类是python用来创建线程的类,
group:扩展保留字段;
target:目标代码,一般是我们需要创建线程执行的目标函数。
name:线程的名字,如果不指定会自动分配一个;
args:目标函数的普通参数;
kwargs:目标函数的键值对参数;
daemon:设置线程是否为守护线程,即是前台执行还是后台执行,默认是非守护线程,当daemon=True时,子线程为守护线程,此时主线程不会等待子线程,如果主线程完成会强制杀死所有的子线程然后退出。

# 方法
start():创建一个子线程并执行,该方法一个Thread实例只能执行一次,其会创建一个线程执行该类的run方法。
run():子线程需要执行的代码;
join():主线程阻塞等待子线程直到子线程结束才继续执行,可以设置等待超时时间timeout.
ident():线程标识符,线程未启动之前为None,启动后为一个int;
is_alive():查看子线程是否还活着你返回一个布尔值。
daemon:判断是否是守护线程;

线程非安全与锁

多个线程之间可以共享内存等资源,使得多个线程操作同一份资源的时候可能导致资源发生破坏,即线程非安全。

number = 100
class MyThread(Thread):

    def run(self):
        for i in range(1000000):
            global number
            number += 1
        print(number)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1(5)

# 结果
1439426
1378835
2241060
2533150
3533150

上例可知,如果是同步运算的话,最终number的结果应该为5000100,但显然不是。原因是如果线程1取得number=100时,线程切换到线程2,又取得number=100,加1赋值给number=101;如果,又切换回线程1,number加1也是101;相当于执行了两次加1的操作,然而number=101.这就是多线程的线程非安全!

怎么解决这个问题呢?我们看到上述代码中number += 1是核心代码,这个地方随意切换线程就会造成数据破坏,因此只要我们能够设置代码每次执行到这里的时候不允许切换线程就行了。这就是锁的由来。

用锁加入上述代码:

number = 100
mutex = threading.Lock() # 创建锁对象

class MyThread(Thread):

    def run(self):
        global number
        for i in range(1000000):
            y = mutex.acquire() # 获取锁
            if y: # 拿到锁就执行下面
                number += 1
                mutex.release() # 释放锁
        print(number)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1(5)

# 结果:
4481177
4742053
4869413
4973771
5000100

可知最后的结果符合预期,threading模块中定义了Lock类,可以很方便实现锁机制,每次执行核心代码之前先去获取锁,拿到了才能执行,拿不到默认阻塞等待。

#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire(blocking=True) # blocking=True,默认线程阻塞等待;如果blocking=False,线程不会等待,即上例中y会返回False,继续执行下面的代码,最后的结果不会符合预期
#释放
mutex.release()
  • 小结
  1. 加锁之后,锁住的那段代码变成了单线程,阻止了多线程并发执行,效率下降了;

  2. 锁可以有多个,如果不同的线程持有不同的锁并相互等待的话,就会造成死锁;

  3. python的多线程问题远不止如此,还有一个历史遗留问题-全局锁。

死锁

如果一段代码存在两个锁的话,可能会出现死锁现象,一旦出现死锁,系统就会卡死。

number = 100
mutex1 = threading.Lock() # 创建锁对象
mutex2 = threading.Lock()

class MyThread1(Thread):

    def run(self):
        global number
        for i in range(1000):
            if mutex1.acquire(): # 拿到锁就执行下面
                number += 1
                if mutex2.acquire():
                    print('this is mutex2')
                    mutex2.release()
                mutex1.release() # 释放锁
        print(number)
class MyThread2(Thread):

    def run(self):
        global number
        for i in range(1000):
            if mutex2.acquire(): # 拿到锁就执行下面
                number += 1
                if mutex1.acquire():
                    print('this is mutex2')
                    mutex1.release()
                mutex2.release() # 释放锁
        print(number)

def get_thread1():
    l_thread = (MyThread1(), MyThread2())
    for t in l_thread:
        t.start()

if __name__ == '__main__':
    get_thread1()

一般解决死锁的办法是尽量不使用多个锁,或设计程序时避免死锁,或为锁添加超时等待。

全局锁(GIL)

全局锁的前世今生不是一两句话能讲完的。可参考:Python全局解释器锁

总结一下就是:

  1. 全局锁的存在是为了保护多线程对数据的安全访问;
  2. 对于任何Python程序,不管有多少的处理器内核,任何时候都总是只有一个线程在执行;
  3. 全局锁的存在使得一般情况下多线程比单线程的执行速度慢;
  4. python程序只有在io密集时多线程代码效率有所提高,所以不推荐使用多线程而是多进程;更好的替代方案为协程;
number = 100
number1 = 100
mutex = threading.Lock()
class MyThread(Thread):

    def run(self):
        global number
        t1 = time.time()
        for i in range(1000000):
            y = mutex.acquire() # 获取锁
            if y: # 拿到锁就执行下面
                number += 1
                mutex.release() # 释放锁
        t2 = time.time()
        print(t2-t1)

def get_thread1(number=5):
    l_thread = (MyThread() for i in range(number))
    for t in l_thread:
        t.start()

def get_thread2(n=5):
    global number1
    for i in range(1000000*n):
        number1 += 1
    print(number1)

if __name__ == '__main__':
    get_thread1()
    t2 = time.time()
    get_thread2()
    t3 = time.time()
    print(t3-t2)

可知多线程的执行时间远远大于单线程。

结论

  • python最好避免使用多线程,而用多进程代替多线程;

  • 协程是多线程的很好的替代方案。

参考:

posted @ 2018-08-30 11:49  倥偬时光  阅读(3890)  评论(0编辑  收藏  举报