python中的线程

目录

线程

1.线程概念

2.进程和线程之间的关系

3.使用threading模块创建线程

1.传递参数

2.join()方法

3.setDaemon() 方法

4.实例方法

5.threading模块提供的方法

7.使用继承方式开启线程

定义一个类继承threading.Thread类。

复写父类的run()方法。

8.线程之间共享全局变量

9.共享全局变量的问题

10.同步异步概念

11.互斥锁

12.死锁

13.线程队列Queue

14.生产者与消费者模型

(1)生产者消费者模式

15.GIL全局解释锁


线程

1.线程概念

由于进程是资源拥有者,创建、撤消与切换存在较大的内存开销,因此需要引入轻型进程即线程,

进程是资源分配的最小单位,线程是CPU调度的最小单位(程序真正执行的时候调用的是线程).每一个进程中至少有一个线程。 

2.进程和线程之间的关系

python中的线程

 

3.使用threading模块创建线程

导入 threading模块

import time

import threading


# 1、首先导入threading模块


def sing():
    for i in range(3):
        print('....正在唱歌', i)
        time.sleep(1)


def dance():
    for i in range(3):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)  # 创建t1子线程,执行sing函数
    t2 = threading.Thread(target=dance)
    t1.start()  # 开启t1子线程
    t2.start()


if __name__ == '__main__':
    main()
    print('程序结束了')

执行顺序:

首先程序运行时,程序从上往下走,遇到main()函数然后开始执行,执行mian()函数的函数体时又创建了两个线程我们称之为子线程,程序运行时的线程我们称之为主线程

然后子线程根据target=xxx 开始执行指定的函数

(等子线程结束后主线程结束,程序结束了)

python中的线程

 

所以输出为:

python中的线程

1.传递参数

给函数传递参数,使用线程的关键字 args=()进行传递参数

python中的线程

import time

import threading


# 1、首先导入threading模块


def sing(num):
    for i in range(num):
        print('....正在唱歌', i)
        time.sleep(1)


def dance(num):
    for i in range(num):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing, args=(3,))  # 给num传递参数
    t2 = threading.Thread(target=dance, args=(3,))
    t1.start()  # 开启t1子线程
    t2.start()


if __name__ == '__main__':
    main()
    print('程序结束了')

 

2.join()方法

join()方法功能:当前线程执行完后其他线程才会继续执行。

import time

import threading


# 1、首先导入threading模块


def sing():
    for i in range(3):
        print('....正在唱歌', i)
        time.sleep(1)


def dance():
    for i in range(3):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)  # 给num传递参数
    t2 = threading.Thread(target=dance)
    t1.start()  # 开启t1子线程
    t1.join()  # 效果:t1执行完后,t2和主线程同时执行
    t2.start()
    # t1.join()  # 效果:t1,t2子线程执行,当t1,t2结束了,主线程才执行


if __name__ == '__main__':
    main()
    print('程序结束了')

输出为:

python中的线程

3.setDaemon() 方法

setDaemon()将当前线程设置成守护线程来守护主线程:

 -当主线程结束后,守护线程也就结束,不管是否执行完成。

 -应用场景:qq 多个聊天窗口,就是守护线程。

注意:需要在子线程开启的时候设置成守护线程,否则无效。

import time

import threading


# 1、首先导入threading模块


def sing():
    for i in range(3):
        print('....正在唱歌', i)
        time.sleep(1)


def dance():
    for i in range(3):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.setDaemon(True)  # 将t1和t2线程设置为守护线程
    t2.setDaemon(True)
    t1.start()  # 开启t1子线程
    t2.start()


if __name__ == '__main__':
    main()
    print('程序结束了')

输出为:

python中的线程

4.实例方法

线程对象的一些实例方法,了解即可

- getName(): 获取线程的名称。

- setName(): 设置线程的名称。

- isAlive(): 判断当前线程存活状态。

import time

import threading


# 1、首先导入threading模块


def sing():
    for i in range(3):
        print('....正在唱歌', i)
        time.sleep(1)


def dance():
    for i in range(3):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)  # 给num传递参数
    t2 = threading.Thread(target=dance)
    print(t1.isAlive())
    print(t1.is_alive())

    t1.start()  # 开启t1子线程
    t2.start()
    print(t1.getName())  # Thread-1
    print(t2.getName())  # Thread-2
    t1.setName('张飞')  # 给线程设置名称
    t2.setName('关羽')
    print(t1.getName())  # 获取线程的名称
    print(t2.getName())


if __name__ == '__main__':
    main()
    print('程序结束了')

输出为:

python中的线程

5.threading模块提供的方法

threading.currentThread(): 返回当前的线程变量。

threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。

threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

import time

import threading


# 1、首先导入threading模块


def sing():
    for i in range(3):
        print('....正在唱歌', i)
        time.sleep(1)


def dance():
    for i in range(3):
        print('....正在跳舞', i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=sing)  # 给num传递参数
    t2 = threading.Thread(target=dance)
    t1.start()  # 开启t1子线程
    t2.start()
    # print(threading.current_thread())
    # print(threading.enumerate())
    print(threading.active_count())

if __name__ == '__main__':
    main()
    print('程序结束了')

7.使用继承方式开启线程

  1. 定义一个类继承threading.Thread类。

  2. 复写父类的run()方法。

 

import threading
import time


# 1、继承threading.Thread类
class MyThread(threading.Thread):

    def __init__(self, num):
        super(MyThread, self).__init__()  # 调用父类的init方法
        self.num = num

    # 2、复写父类的run()方法
    def run(self):
        for i in range(self.num):
            print('i--->', i)
            time.sleep(1)


if __name__ == '__main__':
    my_thread = MyThread(3)
    my_thread.start()

输出为:

python中的线程

8.线程之间共享全局变量

import threading

g_num = 10


def test1():
    global g_num
    g_num += 1
    print('test1---->', g_num)


def test2():
    print('test2---->', g_num)


def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

输出为:

python中的线程

9.共享全局变量的问题

多线程开发的时候共享全局变量会带来资源竞争效果。也就是数据不安全。

import threading

g_num = 0


def test1(num):
    global g_num
    for i in range(num):
        g_num += 1

    print('test1---->', g_num)


def test2(num):
    global g_num
    for i in range(num):
        g_num += 1

    print('test2---->', g_num)


if __name__ == '__main__':
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))
    t1.start()
    t2.start()

 

 

输出为:

python中的线程

注意:

g_num+=1在真正操作的时候会被解析成很多句代码

1、先获取g_num的值

2、获取的值+1

3、把结果保存为g_num

 

模拟cpu执行:首先test1获取了cpu,执行了g_num+=1的前两步,然后test2获取了cpu,执行了g_num+=1的三步。

现丰g_num=1,然后test1又获取到了cpu,执行它的第三步,就是把值保存为g_num,但是此时test2已经修改了g_num的值,所以这一步的操作会将之前的数据覆盖掉。

 

 

10.同步异步概念

同步的意思就是协同步调,按预定的先后次序执行。例如你先说完然后我再说。

大家不要将同步理解成一起动作,同步是指协同、协助、互相配合。

例如线程同步,可以理解为线程A和B一块配合工作,A执行到一定程度时要依靠B的某个结果,于是停下来示意B执行,B执行完将结果给A,然后A继续执行。

A强依赖B(对方),A必须等到B的回复,才能做出下一步响应。即A的操作(行程)是顺序执行的,中间少了哪一步都不可以,或者说中间哪一步出错都不可以。

举个例子:

你去外地上学(人生地不熟),突然生活费不够了;此时你决定打电话回家,通知家里转生活费过来,可是当你拨出电话时,对方一直处于待接听状态(即:打不通,联系不上),为了拿到生活费,你就不停的call、等待,最终可能不能及时要到生活费,导致你今天要做的事都没有完成,而白白花掉了时间。

异步:

异步则相反,A并不强依赖B,A对B响应的时间也不敏感,无论B返回还是不返回,A都能继续运行;B响应并返回了,A就继续做之前的事情,B没有响应,A就做其他的事情。也就是说A不存在等待对方的概念。

举个例子:

在你打完电话发现没人接听时,猜想:对方可能在忙,暂时无法接听电话,所以你发了一条短信(或者语音留言,亦或是其他的方式)通知对方后便忙其他要紧的事了;这时你就不需要持续不断的拨打电话,还可以做其他事情;待一定时间后,对方看到你的留言便回复响应你,当然对方可能转钱也可能不转钱。但是整个一天下来,你还做了很多事情。 或者说你找室友临时借了一笔钱,又开始happy的上学时光了。

对于多线程共享全局变量计算错误的问题,我们可以使用线程同步来进行解决。

 

11.互斥锁

当多个线程几乎同时修改一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全的访问竞争资源(全局内容),最简单的同步机制就是使用互斥锁。

某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定状态,其他线程就能更改,直到该线程将资源状态改为非锁定状态,也就是释放资源,其他的线程才能再次锁定资源。互斥锁保证了每一次只有一个线程进入写入操作。从而保证了多线程下数据的安全性。

1.练习一使用互斥锁解决200万次的计算问题。

import threading

g_num = 0


def test1(num):
    global g_num
    lock.acquire()  # 加锁
    for i in range(num):
        g_num += 1
    lock.release()  # 解锁
    print('test1---->', g_num)


def test2(num):
    global g_num
    lock.acquire()  # 加锁
    for i in range(num):
        g_num += 1
    lock.release()  # 解锁
    print('test2---->', g_num)


lock = threading.Lock()


def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

 

python中的线程

输出为:

python中的线程

12.死锁

在多个线程共享资源的时候,如果两个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁现象。

如果锁之间相互嵌套,就有可能出现死锁。因此尽量不要出现锁之间的嵌套。

如果双方都不开口,双方就一直等待。

import threading
import time


def test1(num):
    # lock1上锁
    lock1.acquire()
    print('test1...')
    time.sleep(1)
    # lock2上锁
    lock2.acquire()
    print('test2...>>>')
    # lock2解锁
    lock2.release()
    # lock1解锁
    lock1.release()


def test2(num):
    lock2.acquire()
    print('test2...')
    lock1.acquire()
    print('test2...>>>')
    lock1.release()
    lock2.release()


lock1 = threading.Lock()
lock2 = threading.Lock()


def main():
    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

输出为:

 

python中的线程

13.线程队列Queue

队列是一种先进先出(FIFO)的存储数据结构,就比如排队上厕所一个道理。

1.创建一个“队列”对象

import Queue # 导入模块

q = Queue.Queue(maxsize = 10)

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

2.将一个值放入队列中 q.put(10)

调用队列对象的put()方法在队尾插入一个项目。

3.将一个值从队列中取出q.get()

从队头删除并返回一个项目。如果取不到数据则一直等待。

4.q.qsize() 返回队列的大小

5.q.empty() 如果队列为空,返回True,反之False

6.q.full() 如果队列满了,返回True,反之False

7.q.put_nowait(item) ,如果取不到不等待,之间抛出异常。

8.q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号

9.q.join() 收到q.task_done()信号后再往下执行,否则一直等待。或者最开始时没有放数据join()不会阻塞。

q.task_done() 和 q.join() 通常一起使用。

import queue

#创建线程队列对象
q = queue.Queue(3)
q.put('haha')
q.put(123)
q.put([1, 2, 3])
print(q.qsize())
# q.put('xxx')
print(q.get())
print(q.get())
print(q.get())
# print(q.get())
# print(q.get_nowait())  # queue.Empty异常
print(q.empty())
print(q.full())
q.task_done()
q.join()
print('------')

14.生产者与消费者模型

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

 

(1)生产者消费者模式

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这就像,在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可。

import queue, threading

q = queue.Queue()


def producer():
    count = 1
    while count <= 100:
        q.join()  # 等待take_done()发送的信号
        q.put(count)
        print('正在烤第{}只鸭子'.format(count))
        count += 1


def customer():
    count = 1
    while count <= 100:
        data = q.get()
        print('正在吃第%d只鸭子' % data)
        count += 1
        q.task_done()  # 吃完之后发送信号


def main():
    t1 = threading.Thread(target=producer)
    t2 = threading.Thread(target=customer)
    t1.start()
    t2.start()


if __name__ == '__main__':
    main()

输出为:

python中的线程

 

 

15.GIL全局解释锁

GIL 即 :global interpreter lock 全局解释所。

在进行GIL讲解之前,我们可以先了解一下并行和并发:

并行:多个CPU同时执行多个任务,就好像有两个程序,这两个程序是真的在两个不同的CPU内同时被执行。

并发:CPU交替处理多个任务,还是有两个程序,但是只有一个CPU,会交替处理这两个程序,而不是同时执行,只不过因为CPU执行的速度过快,而会使得人们感到是在“同时”执行,执行的先后取决于各个程序对于时间片资源的争夺.

并行和并发同属于多任务,目的是要提高CPU的使用效率。这里需要注意的是,一个CPU永远不可能实现并行,即一个CPU不能同时运行多个程序。

Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核cpu,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁, 于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了保证同一个时刻只允许一个线程可以使用cpu。也就是说多线程并不是真正意义上的同时执行。