进程:承担分配资源实体的基本单位。
线程:调度的基本单位。线程是进程内部一条执行流。线程在进程的地址空间运行。
所有的一整块叫进程,每一个task_struct是一条执行流。
在Linux中,没有专门为线程设计数据结构,线程是用task_struct模拟出来的。而轻量级进程就和他关联起来。就可以实现内核调度用户创建的线程了。
注意区分多进程和多线程,多线程一份地址空间,多进程多个地址空间。
线程的优点:
线程的缺点:
程序执行时,一个进程一个执行流,经过用户创建线程,变成一个进程多个执行流,而其中一个执行流出现异常,操作系统发信号,进程接收到信号,释放资源,全部线程都不复存在
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID(用户级就是那一串地址,内核级就是lwp)
一组寄存器(保存上下文,可以进程切换)
栈(就是用户库里实现的,变量不冲突)
errno(全局变量,临界资源,所以需要各自私有)
信号屏蔽字(block集,pending没有私有)
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
多进程强调独立但不是完全独立,比如fork之后,进程间通信。
多线程强调共享但不是完全共享,他也要有自己私有的数据。
P-thread库采用prosix标准,是一个用户级别库
Linux没有关于线程的数据结构,所以没有创建线程的接口,但是他有创建轻量级进程的接口,线程会与轻量级进程关联起来。P-thread库就是第三方提供的一套库,所以链接的时候需要链接这个库。
他为什么不加-i,-L,那些呢,因为他是在系统默认目录下的。
而在用户的库,也要对这个线程先描述在组织。描述用结构体描述,组织用数组
#include
#include
#include
void* thread_run(void *args)
{
while(1)
{
printf("i am %s\n",(char*)args);
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
while(1)
{
printf("i am main pthread\n");
sleep(1);
}
return 0;
}
两个循环同时打印,因为有两个执行流,其中main中的叫做主执行流。
当./运行这个程序的时候,内存中就有一个进程,两个执行流。
加入getpid接口,返现他们的pid一致。
ps -aL,查看
lwp为轻量级进程,而一个轻量级进程调用一个线程,cpu调度是以他为基本单位,线程同属于一个线程组(大进程),getpid返回为线程组的pid。为所以操作系统就能区分你们是同一个进程的两个执行流
需要注意的是
main函数中return相当于exit(),相当于终止进程,进程的地址空间都没了,肯定都完蛋了。
但是主线程调用pthread_exit的话是不会影响别的线程的。
这两种都是线程自己主动退出。
这种是通过pthread_cancel,可以别的线程通过你的用户级线程id来终止线程。
在主线程中等待create创建的线程,在thread 1 sleep的10s中,主线程一直在阻塞式等待。
和进程类似,线程退出,没有其他进程等待,也会造成类似僵尸进程一样的结果,导致内存泄漏。所以需要等待。
其他行为和进程类似吗?在之前学到,一个进程退出方式有三种,代码运行完,结果正确。代码运行完结果错误,进程异常终止。前两个可以获取退出码,后一个可以可以查看退出信号。
那线程等待,也是一样吗。肯定不一样,任意一个线程异常,整个进程直接结束,因为操作系统是向进程发信号的。
所以线程等待,只关心他的退出码,也就是我们只关系线程退出时正确与否的退出码,异常是不关心的,你一错进程去背锅。因为信号是给进程设计的。也侧面说明了,虽然block表线程各自私有,pending表属于整个进程。
线程终止有三种情况,那么对应的join等待也会得到3种不同的状态。还有我们不关系他的状态可以设置为NULL。
可以自己把自己分离。
也可以别人将你分离
这种经过分离的线程就不需要在pthread_wait,在他结束后自动回收资源。
虽然分离!!但是线程异常也会影响其他线程。
在线程中,子进程不修改这个数据时,数据只有一份与父进程共享,当修改时发生写时拷贝将数据私有。这个两个数据的虚拟地址一样,但是虚拟地址空间不是一套,经过页表转换后的物理地址不同。
但在线程中,由于线程共享地址空间。此时代码中a为全局变量,全局变量处于数据段不私有。所以他们即使修改了,也访问的是同一个数据。
#include
#include
#include
int a=10;
void* pthread_run(void* str)
{
while(1)
{
sleep(1);
printf("%s, %d\n",(char*)str,a);
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,pthread_run,(void*)"mythread1");
pthread_create(&t2,NULL,pthread_run,(void*)"mythread2");
pthread_create(&t3,NULL,pthread_run,(void*)"mythread3");
pthread_create(&t4,NULL,pthread_run,(void*)"mythread4");
sleep(5);
a=20;
sleep(5);
//前5s打印10,后5s打印20
//
//
//写上等待规范一点,实际上那个是死循环,不退出。
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
return 0;
}
一个地址空间,数据段没有私有,看到同一个数据,虚拟内存,物理内存一致。
a就是一个临界资源。printf是临界区
在实际生活中是怎么抢票的呢?
当票大于0,用户购买,票数–,当票数小于0,那么就wait等待重新放票。ticket叫做临界资源,if与距离的就叫做临界区。
ticket–,++,都不是原子性,因为它要经历三个过程,对应三条汇编指令。
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
在这三个过程当中,ticket肯定有未发生变化的过程,所以当另一个线程来访问它的时候,就可能访问的是未修改的值。就有可能多个用户抢到一张票。甚至只剩一张票时,多个执行流对ticket>0,进行判断时多个用户拿到同一个最后一张票,因为ticket–并不是原子性的。
所谓原子性就是只有两态,要么有,要么没有,其实也可以有中间状态,但是只要你不影响其他人,也可以认为你具有原子性。
我们可以模拟一个抢票的多线程程序,来看看
#include
#include
#include
int ticket=100;
void* route(void* args)
{
while(1)
{
usleep(1000);
if(ticket>0)
{
ticket--;
printf("thread %d get %d \n",(int)args,ticket);
}
else{
break;
}
}
}
int main()
{
pthread_t tid[4];
int i=0;
for( i=0;i<4;i++)
{
pthread_create(&tid[i],NULL,route,(void*)i);
}
int j=0;
for(j=0;j<4;j++)
{
pthread_join(tid[j],NULL);
}
return 0;
}
本质上下面三种方法可以解决。
要做到这三点,Linux提供了一把mutex锁,叫做互斥量。锁也有很多,pthread有对应的数据结构描述,组织。
要给线程加锁,首先要让他们看到同一把锁
pthread_mutex_t lock;
pthread_mutex_init(&lock,NULL);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_mutex_t destory(&lock);
那么将刚才有问题的抢票代码,修改加上锁。
#include
#include
#include
int ticket=100;
pthread_mutex_t lock;
void* route(void* args)
{
while(1)
{
usleep(1000);
pthread_mutex_lock(&lock);
if(ticket>0)
{
ticket--;
printf("thread %d get %d \n",(int)args,ticket);
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
//假如在这里解锁,假如在之前break了,进程带着锁跑了,这个解锁就不执行了。
//pthread_mutex_unlock(&lock);
}
}
int main()
{
pthread_t tid[4];
pthread_mutex_init(&lock,NULL);
int i=0;
for( i=0;i<4;i++)
{
pthread_create(&tid[i],NULL,route,(void*)i);
}
int j=0;
for(j=0;j<4;j++)
{
pthread_join(tid[j],NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
lock:
当每个线程执行第一步时,由于线程的寄存器私有,互不影响
第二步
交换寄存器和mutex的值,那么此时来了其他线程会不会影响呢?
也是不会的,第二个线程来,执行第一条语句寄存器置为0,mutex并不私有,交换mutex的值,mutex的值此时为0,那么0和0交换,此时他为0,执行else于是挂起等待。在这个过程中和传统的内存到寄存器不同,这里的是exchange交换,并不是传统的拷贝,实际mutex为1的时候,只有一个进程会有。
然后申请到锁的那个线程,切换回来执行if申请成功。
unlock:
能解锁的一定是加过锁的,能走到这个unlock的语句的代码,一定只有一条。
假如多个线程调用一个函数,导致出错。那么这个函数叫做不可重入函数,发生的情况叫线程安全。
他们的情况也可以多了解。
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
两个线程,线程1,拥有1锁申请2锁,线程2拥有2锁,申请1锁,且双方互不释放锁。
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
产生死锁,一定是由于着四个条件同时发生。破坏死锁只需要破坏其中一个条件就好。
避免死锁
一个线程一把锁,也可以产生死锁,当你还未释放时,再次申请锁,由于锁被自己拿着,没有释放,是申请不到的,一直被挂起。