一:多任务的概念
当计算机运行一个程序时,如果没有开启线程,那么整个程序就是单线程的,单线程的意思就是程序在main()函数中是顺序执行的,那么在小型程序中或许单线程执行没什么问题,但是如果遇到类似网络编程就会遇到阻塞的情况,这个时候,如果程序可以并行执行话,就可以方便很多。所以引入了线程机制。
二:python中的线程
在python中想要使用线程,先引入threading模块,其实thread也可以,但是threading把thread又进行了一层封装。所以编程常用threading模块
import threading
import time
def saySorry():
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() #启动线程,即让线程开始执行
上面这段代码就是对于线程最简单的应用,可以看出,在for循环中创建了5个线程,threading模块中的thread类创建线程,参数有你想并行执行的函数名,还有参数,只是这里没有表示出来。这5个线程都执行一个函数saySorry。target就相当于callback,属于回调机制。这里要注意一点的就是,当线程对象调用start()方法时,线程才被真正的创建并执行。
线程的生命周期:一般来说,如果你创建了很多线程,但是呢,你的主线程并没有阻塞住并等待子线程结束的话,那么当主线程结束后,不论子线程执行到哪里,都会退出。所以一般调用线程里的别的方法,来使主线程等到子线程都结束的时候,在结束程序。
线程的执行顺序:操作系统对于线程和进程的调度是我们无法控制的,也就是说,没有顺序。所以如果我们非要让他有点顺序,那就要用别的方法去控制,例如sleep等等。
用面向对象的方法实现线程:
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start()
上述代码就是面向对象方法实现一个线程,不难理解,我们新建的这个线程类,继承了threading模块中的thread方法,在里面我们只要重写父类的run方法就可以实现线程。
线程参数传递:
from threading import Thread
import time
def work1(nums):
nums.append(44)
print("----in work1---",nums)
def work2(nums):
#延时一会,保证t1线程中的事情做完
time.sleep(1)
print("----in work2---",nums)
g_nums = [11,22,33]
t1 = Thread(target=work1, args=(g_nums,))
t1.start()
t2 = Thread(target=work2, args=(g_nums,))
t2.start()
从上述代码可以看出,args参数是一个元组,我们把我们想传递给线程的参数放到这个元组中,那么在线程函数里我们就可以使用了。
线程间共享资源:
from threading import Thread
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2():
global g_num
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = Thread(target=work1)
t1.start()
#延时一会,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
上述代码的运行结果为
---线程创建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---
可以看出,在一个程序内部,各线程共享全局变量,也可以在线程内部改变这个共享资源。这是一把双刃剑,即提供了便利,也提升了代码产生混乱的机率。
三 互斥锁
那么在线程要用共享资源的这一情境下,互斥锁应运而生。其实互斥锁只是标志位的高级抽象。我们可以这么来思考这个互斥锁的概念,就好比我们都要去上厕所,甲先进去,但是如果门不锁的话,乙并不知道里面有人,乙也会进去,这样的情况是不允许发生的,所以,甲在上厕所的时候,就把门锁好,出来的时候,就把门解锁。那么以上的这个流程,就是互斥锁的应用,其实如果不用python或者linux内部的API我们依然可以编写代码来自己实现锁机制,怎么实现?就是定义一个全局变量,初始值为0,当甲在用的时候我们把这个变量加一,那么如果此时乙也要来竞争这个资源,那么当他发现,这个全局变量为1的时候,他就等待,直到甲出来,把全局变量再次变成0的时候,乙才能去使用这个资源。那么加一减一的操作又叫做原子操作。实际上,锁的机制在内部的原理也大致如此。那么下面放上一段代码:
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test2---g_num=%d"%g_num)
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
# 等待计算完成
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
这里再说一件事情,那就是,我们在上锁的时候,尽量减少锁内部的代码,这样,运行起来效率高,而且也便于维护。
四 死锁:
那么什么是死锁呢,死锁就是我的等待你的资源先释放,你等待我的资源先释放。我们知道的是,计算机并没有退让意识,那么这种情况之下,就会死住。代码如下:
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
A锁里有等待B的资源释放,B锁里等A的资源先释放,那么这段代码运行之后,就会卡死。
三 linux下的线程使用
1.线程的生命周期 ****
线程的创建,可以在主线程中或其它线程中任意位置来创建
线程的结束,就是线程函数的结束
线程的生命周期包括:就绪、运行、阻塞、终止
就绪:线程能够运行,但是在等待可用的处理器。可能刚刚启动,或者刚刚从阻塞中恢复,或者被其它线程抢占。
运行:线程正在运行。在单处理器系统中,只能有一个线程处于运行状态,在多处理器系统中,可能有多个线程处于运行态。
阻塞:线程由于等待“处理器”外的其它条件而无法运行,如:条件变量的改变,加锁互质量或者等待I/O操作结束。
终止:线程从线程函数中返回,或者调用pthread_exit,或者被取消,或随进程的终止而终止。线程终止后会完成所有的清理工作。
就是线程的编号
线程ID用于唯一识别哪一个线程。线程ID用pthread_t类型来表示,在线程中,可以用如下函数获取线程ID:
//获取线程ID
Pthread_t pthread_self(void);
在线程内部读取,其它线程无法读取
1)线程的创建
#include <pthread.h>
//创建线程
int pthread_create(pthread_t *thread,const pthread_attr_t *attr, void *(*start_routine)(void*),void * arg);
参数:thread 线程id的地址,在创建线程时,产生ID
attr 属性 优先级
start_routine 用来指定线程函数 void *(*)(void*)类型代表了函数必须有void* 返回,必须有void*形参
arg 是线程函数的参数
2)线程的终止
(1)线程函数自然结束 自然死亡
(2)在线程函数中使用return 自杀
线程函数内调用下面函数
void pthread_exit(void *value_ptr);
(3)在其它线程使用下面函数 他杀
int pthread_cancel(pthread_t thread);
(4)随进程终止而终止 陪葬
3)等待线程结束 收尸
//等待线程结束,如果这个线程没有结束,则函数处于阻塞模式
int pthread_join(pthread_t thread, void **value_ptr);
参数: thread 已创建的线程ID号
value_ptr: 是线程返回值的地址
用法:
需要代码并行执行,则创建线程
线程函数:循环(超过0.2秒) 如音频播
耗时代码 (超过0.2秒)
1)线程的简单用法.
//mplay.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int (*sendstate)(int,int);
static int state; //0 停止 1播放 2暂停
static pthread_t id;
void* run(void* arg){
int i=0;
int size=180;
while(state){
while(state==2) usleep(20000);//微秒
//1) 读取磁盘文件
//2) 解码
//3) 写入声卡
//4) 调用回调,将数据传入到UI层
sendstate(state,i++);
if (i>=size) break;
sleep(1);
}
state=0;
sendstate(state,i++);
return (void*)120;
}
int play(char *file,int (*handler)(int,int)){
if (state==0){
//1.保存函数地址到全局
sendstate=handler;
state=1;
//2.启动一个循环
pthread_create(&id,NULL,run,NULL); //创建线程
}
else state=1;
}
int pause(){
state=2;
sendstate(state,0);
}
int stop(){
state=0;
sendstate(state,0);
int *a;
pthread_join(id,&a);//等待结束
printf("---- %d\n",a);//值为120
}
2)线程函数的参数.
格式
void * 函数名(void *参数){
}
函数参数,是在创建线程由arg决定
传整型
void *run(void* arg){
int a=*((int*)arg);
while(1){
printf("------- pthread %d\n",a);
sleep(1);
}
}
int main(int argc,char **argv){
//pthread_t id[5];
int i;
for(i=0;i<5;i++){
pthread_t id;
pthread_create(&id,NULL,run,&i); //非阻塞
usleep(200);
}
int b;
scanf("%d",&b);
}
传数组
void *run(void* arg){
int p[7];
memcpy(p,arg,4*7);
int i=0;
while(1){
printf("------- pthread %d\n",p[i++%7]);
sleep(1);
}
//free(p);
}
int main(int argc,char **argv){
int a[]={1,2,3,4,5,6,7};
int i;
//for(i=0;i<5;i++){
pthread_t id;
pthread_create(&id,NULL,run,a); //非阻塞
usleep(200);
//}
int b;
scanf("%d",&b);
}
原则,尽快的将arg指向的内存数据拷到线程函数,以免在主函数中将arg指向的值改变。
3)线程属性的设置
线程创建时,默认优先级为0 最大为50
pthread_attr_t属性的类型
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数,优先级
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; //设置线程栈的地址
size_t stacksize; //设置线程栈的大小
}pthread_attr_t;
//初始化线程属性
int pthread_attr_init ( pthread_attr_t *attr );
//销毁线程属性
int pthread_attr_destroy ( pthread_attr_t *attr );
(2) 线程优先级
//设置优先级结构体
int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr,struct sched_param *param);
struct sched_param结构如下:
struct sched_param{
int __sched_priority; //所要设定的线程优先级
};
int main(int argc,char **argv){
int i;
pthread_t id[5];
pthread_attr_t attr;
pthread_attr_init (&attr);
struct sched_param par={50};
pthread_attr_setschedparam(&attr,&par);
for(i=0;i<5;i++){
if (i==2) pthread_create(&id[i],NULL,run,&i); //创建线程,默认优先级
else pthread_create(&id[i],&attr,run,&i); //创建线程,同时设优先级
usleep(200000);
}
pthread_attr_destroy(&attr);
int b;
scanf("%d",&b);
}