在学习了进程过进程之后,我们就该来学习一下线程了,什么是线程呢?接下来我们就来学习一下。
1、线程的概念
我们都知道,进程就是程序运行起来的实体,包括一大堆的数据结构,而线程是什么呢?我们应该听过一句话“线程是在进程内部运行的”,如何理解这句话呢?接下来带着问题我们来学习。
(1)线程的概念
什么是线程?简单来说,线程就是一个程序里的执行流,更准确的说:线程是“一个进程内部的控制序列”。一个进程至少有一个执行线程(即主线程)。
(2)线程和进程
>线程ID>一组寄存器>栈>errno>信号屏蔽字>调度优先级
在线程中,线程拥有自己的线程ID在内核中由LWP表示;线程私有的一组寄存器存有独立的上下文数据;以及私有栈空间;当然也就有了私有的调度优先级;
(3)一进程的多线程共享
(4)线程的优点
(5)线程的缺点
在了解了线程之后,我们再来谈谈进程。我们在前边学习了进程,在前边,我们知道进程不简单的是一段程序它是一段跑起来的程序,是一个实体,当我们学习了线程之后,进程就不能直接叫进程了。
在Linux下,进程叫做轻量级进程;Linux下的一个进程由一个或多个PCB、以及资源组成;Linux下进程是承担分配系统资源的实体;而在Linux下,调度的基本单位不再是进程而是线程;“线程是在进程内部运行的”这句话现在我们就能理解了,因为进程和线程共享地址空间,所以线程在进程内部运行也就是线程在进程的地址空间中。在Linux下,进程控制块和线程控制块都由PCB表示,而在Windows操作系统下,线程控制块叫做TCB。
2、可重入函数和线程安全
可重入和线程安全的概念?它们两个有什么区别和联系?
一个函数被称为线程安全的,当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。反之,如果一个函数不是线程安全的,我们就说它是线程不安全的。线程不安全有四种情况:
可重入函数即表示可以被多个执行流重复进入。
可重入函数是线程安全的一种。
线程安全和可重入的区别:
1、可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
可重入函数与线程安全的区别与联系:
2、线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说。
3、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
4、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
5.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
6、线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。
3、线程控制
我们现在所学的线程都是在用户级库(POSIX线程库)中使用,与线程有关的函数构成了一个完整的系列,绝大数函数的名字都是以“pthread_”打头的,要使用这些数据库,要通过引入头文件
(1)创建线程
#include
int pthread_creat(pthread_t *thraed , const pthread_attr_*attr ,
void *(*start_routine)(void*) , void *arg);
参数:thread 返回线程ID;
attr 设置线程属性,默认为NULL;
start_routine 为回调函数地址,线程启动后执行;
arg 传给回调函数的参数;
返回值:成功返回0,失败返回错误码;
错误检查:
下面用实例来展示一下:
creat.c
#include
#include
#include
#include
#include
void *Pthread_run(void *arg)
{
int i=0;
for(;;)
{
printf("I'm thread 1 \n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int ret=pthread_create(&tid,NULL,Pthread_run,NULL);
if(ret!=0)
{
fprintf(stderr,"pthread_creat :%s \n",strerror(ret));
exit(EXIT_FAILURE);
}
int i=0;
for(;;)
{
printf("I'm main thread\n");
sleep(1);
}
return 0;
}
makefile
creat:creat.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f creat
(2)进程ID和进程ID
在Linux中,目前的线程实现是Native Thread Libaray,简称NPTL。在这种实现下,线程又称为轻量级进程,每一个用户态的线程,在内核中都对应一个调度实体,因为拥有自己的进程描述符。
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID、但是在引入线程概念之后,情况发生了变化,一个用户进程下管理N个用户态线程,每个线程作为一个独立的调度实体在内核中拥有自己的进程描述符,进程和内核的描述符一下子变成了1:N的关系。
Linux内核引入了线程组的概念。
多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID ,该值对应的是用户层面的进程ID。
用户态 | 系统调用 | 内核进程描述符中对应的结构 |
线程ID | pid_t gettid(void) | pid_t pid |
进程ID | pid_t getpid(void) | pid_t tgid |
现在说的进程ID,不同于pthread_t 类型的线程ID,和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整形变量。
查看一个线程的ID:
(3)线程ID及进程地址空间布局
pthread_t pthread_self(void);
pthread_t的类型取决于实现,对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
(4)线程终止
我们知道,进程退出有三种情况,那么线程呢?线程退出和进程退出有一点不同,就是线程退出不存在代码没跑完异常退出的情况,也就是说,线程退出有两种情况,一是代码跑完结果正确,二是代码跑完结果不正确。
线程退出的三种方式:(值终止线程,而不终止进程)
下面说一说线程退出的两个函数:
void pthread_exit(void *value_ptr); 线程终止函数
参数:value_ptr 不要指向一个局部变量;
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身);
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。
int pthread_cancel(pthread_t thread);取消一个执行中的线程
参数:thread 线程ID
返回值: 成功返回0,失败返回错误码;
线程被取消,返回-1
4、线程等待与分离
(1)线程等待
为什么需要线程等待?
已经退出的线程所占有的空间未被释放,任然存在于进程的地址空间内,造成内存泄漏;创建新的线程时是不会复用刚才退出的线程的地址空间;如果不等待,主线程直接退出,就标志着进程退出,线程根本没有机会来执行代码,就会被释放。
线程等待函数:
int pthread_join(pthread_t thread, void **value_ptr );
参数:thread 线程ID
value_ptr指向一个指针,而该指针指向线程的返回值;
返回值:成功返回0,失败返回错误码;
调用该函数的线程将挂起等待,知道ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的进程终止状态是不同的:
(2)分离线程
默认情况下,一个新创建的线程是可结合的(joinable),意思就是在线程结束后,需要调用pthread_join函数进行等待回收,否则无法回收释放资源,从而造成内存泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出后,自动释放线程资源。
int pthread_detachn(pthread_t thread);
分离线程可以是自己提出的,也可以由主线程对该线程进行分离;
pthread_detach(pthread_self);
一个线程被分离,就不需要被pthread_join,joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
举栗子说明自己分离:
#include
#include
#include
#include
#include
void *thread_run(void *arg)
{
pthread_detach(pthread_self());
printf("%s \n",(char *)arg);
return NULL;
}
int main()
{
pthread_t tid;
int id=pthread_create(&tid,NULL,thread_run,"thread 1 is running\n");
if(id!=0)
{
printf("creat thread failure\n");
return 1;
}
int ret=0;
sleep(1);
if(pthread_join(tid,NULL)==0)
{
printf("waiting succeed\n");
ret=0;
}
else
{
printf("waiting failed\n");
ret=1;
}
return ret;
}
结果是先出现一行“thread 1 is running”,然后一秒后,等待失败。原因就是在那一秒中,thread 1已经把自己给分离了。
一个线程出错挂掉,则相对应的进程也会挂掉;一个被分离的线程异常挂掉,进程依旧也会退出。
5、线程同步与互斥
在学习进程间通信时,我们知道要想让进程间进行通信,就必须让两个进程同时看到同一份公共资源,也就是临界资源,然后才有可能进行进程间通信,但是,当两个进程进行通信时会产生一些小问题,所以我们就说到了同步与互斥,当然两个线程间进行通信时,不必那么麻烦,因为它们两个本来就在同一个进程中,大多数资源都是共享的,但是只要有共享的地方,肯定就会出现这样那样的问题,所以,线程间也需要同步与互斥机制。
(1)mutex互斥量
大部分情况下,线程使用的数据都是局部变量,变量的地址空间在线程自己私有栈空间内,这种情况,变量归属于单个线程,其他线程无法获得这些变量。但有时候,很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。但是,多个线程并发的操作共享变量,会会带来一些问题。
例如下面操作共享变量而出现问题的售票系统:
#include
#include
#include
#include
#include
int ticket=100;
void *sellticket(void *arg)
{
char *tid=(char *)arg;
while(1)
{
if(ticket>0)
{
usleep(1000);
printf("%s sells ticket:%d \n",tid,ticket);
ticket--;
}
else
{
break;
}
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,sellticket,"thread 1");
pthread_create(&t2,NULL,sellticket,"thread 2");
pthread_create(&t3,NULL,sellticket,"thread 3");
pthread_create(&t4,NULL,sellticket,"thread 4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
}
你会发现结果有些不对劲,怎么还会有负数的票,用实际来说就是一张票卖给了不同的两个人,这个怎么可能?根本不符合现实,为什么会这样呢?
if语句判断条件为真以后,代码可以并发的切换到其他线程;在usleep 的过程中,可能会有很多个线程会进入该代码段;而且ticket--这个操作也不是一个原子操作,ticket--这一句C语言代码对应的汇编代码有三条,先将ticket变量由内存加载到寄存器中,然后更新寄存器里面的值(即进行-1操作),再将减完后的值写回到内存中。
要解决以上的问题,就要做到:
要做到这三点,本质上就需要一把锁,而在Linux中提供的这把锁叫互斥量。
而锁要在进入临界区之前加,在出了临界区之后就解锁。
(2)互斥量的接口
初始化互斥量:
静态分配:定义一个宏,直接赋值
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配:使用库函数进行初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex , const pthread_mutexattr_t *restrict attr);
参数:mutex 需要初始化的互斥量
attr 默认为NULL
销毁互斥量:
销毁互斥量需要注意:静态分配的互斥量不需要销毁;不要销毁一个已经加锁的互斥量;已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t * mutex );
互斥量加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t * mutex );
int pthread_mutex_unlock(pthread_mutex_t * mutex );
返回值:成功返回0,失败返回错误码;
调用pthread_lock时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没竞争到互斥量,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。
给上面的例子加个互斥量改进一下:
#include
#include
#include
#include
#include
#include
int ticket=100;
pthread_mutex_t mutex;
void *sellticket(void *arg)
{
char *tid=(char *)arg;
while(1)
{
pthread_mutex_lock(&mutex);
if(ticket>0)
{
usleep(1000);
printf("%s sells ticket:%d \n",tid,ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1,NULL,sellticket,"thread 1");
pthread_create(&t2,NULL,sellticket,"thread 2");
pthread_create(&t3,NULL,sellticket,"thread 3");
pthread_create(&t4,NULL,sellticket,"thread 4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
pthread_mutex_destroy(&mutex);
}
(3)条件变量
当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。例如当一个线程线程访问队列时,发现队列为空,它只能等待,只到其他线程将一个节点添加到队列中,而这种时候就需要用到条件变量。这个条件变量将相当于给这个线程发送的信号一样,当该线程接收到信号时,就知道队列不为空了,然后就可以访问了。
条件变量函数:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 初始化
参数:cond 要初始化的变量
attr 默认为NULL;
int pthread_cond_destroy(pthread_cond_t *cond); 销毁
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t
*restrict mutex); 等待条件满足 等待时释放锁
int pthread_cond_broadcast(pthread_cond_t *cond );广播给全部线程通知
int pthread_cond_signal(pthread_cond_t *cond );给某个线程单独通知
简单案例
#include
#include
#include
#include
#include
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1(void *arg)
{
while(1)
{
pthread_cond_wait(&cond,&mutex);
printf("active \n");
}
}
void *r2(void *arg)
{
while(1)
{
pthread_cond_signal(&cond);
sleep(1);
}
}
int main()
{
pthread_t t1,t2;
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL);
pthread_create(&t1,NULL,r2,"thread 1");
pthread_create(&t2,NULL,r1,"thread 2");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
结果就是每隔一秒打印一个active。
为什么pthread_cond_wait需要互斥量?
其实简单来说就是用来释放锁,监测条件变量。但必须是原子的。
(4)条件变量使用规范
等待条件代码:
pthread_mutex_lock(&mutex);
while(条件为假)
pthread_cond_wait(cond,mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_inlock(&mutex);
(5)生产者消费者模型
生产者消费者模型简单来说就是“321”原则,“321”原则就是三种关系(生产者与生产者之间互斥关系,消费者与消费者之间互斥关系,消费者与生产者之间互斥且同步),两种角色(生产者与消费者),一个交易场所(可以是各种数据结构,链表,数组)。
最简单的就是一个生产者一个消费者,然后再加一个交易场所。(两个线程来模拟生产者消费者模型)---基于链表
#include
#include
#include
#include
#include
#include
typedef struct linklistNode
{
int key;
struct linklistNode *Next;
}Node,*pNode,**ppNode;
pNode head;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
void Initlinklist(ppNode head)
{
assert(head);
(*head)->Next=NULL;
}
int isEmpty(pNode head)
{
return head->Next==NULL;
}
pNode NewNode(int key)
{
pNode node=(pNode)malloc(sizeof(Node));
if(node!=NULL)
{
node->key=key;
node->Next=NULL;
}
return node;
}
void PushFront(pNode head,int key)
{
assert(head);
if((head->Next)==NULL)
{
head->Next=NewNode(key);
return;
}
pNode newnode=NewNode(key);
newnode->Next=head->Next;
head->Next=newnode;
}
void PopFront(pNode head,int * key)
{
assert(head);
if(head==NULL)
{
return;
}
pNode del=head->Next;
*key=del->key;
head->Next=del->Next;
free(del);
}
void Destroy(pNode head)
{
assert(head);
pNode cur=head;
pNode del=NULL;
while(cur!=NULL)
{
del=cur;
cur=cur->Next;
free(del);
}
head->Next=NULL;
}
void *Productor(void *arg)
{
int key=0;
while(1)
{
pthread_mutex_lock(&lock);
key=rand()%101;
PushFront(head,key);
printf("Productor push %d\n",key);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);
sleep(5);
}
}
void *Customer(void *arg)
{
int key=0;
while(1)
{
pthread_mutex_lock(&lock);
while(head->Next==NULL)
{
printf("waiting for date\n");
pthread_cond_wait(&cond,&lock);
}
PopFront(head,&key);
printf("Customer pop %d\n",key);
pthread_mutex_unlock(&lock);
//sleep(1);
}
}
int main()
{
head=(pNode)malloc(sizeof(Node));
Initlinklist(&head);
pthread_t t1,t2;
srand(time(NULL));
pthread_create(&t1,NULL,Productor,NULL);
pthread_create(&t2,NULL,Customer,NULL);
Destroy(head);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}
我实现的是消费者快,而生产者慢的实例,所以当生产者生成一个,消费者拿走之后,其余四秒钟消费者都在等待生产者生产。
(6)POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的,单POSIX可以用于线程间同步。
初始化信号量:
#include
int sem_init(sem_t *sem, int pshared,unsigned int value);
参数: pshared 0表示线程间共享,非0表示进程间共享;默认为0;
value 信号量初始值;
销毁信号量:
int sem_destroy(sem_t *sem);
等待信号量:
int sem_wait(sem_t *sem); 等待信号量,若是等待成功信号量的值减1,相当于P操作;
发布信号量:
int sem_post(sem_t *sem); 发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1;
上边的消费者生产者模型是基于链表的,其空间可以等他分配,现在写一个基于固定大小的循环队列重写消费者生产者模型。(循环结构可以用数组+模运算实现)
#include
#include
#include
#include
#define SIZE 1024
//环形队列
int arr[SIZE] = {0};
sem_t sem_pro; //描述环形队列中的空位置
sem_t sem_con; //描述唤醒队列中的数据
//生产者,只要环形队列有空位,便不断生产
void*productor(void*arg){
int data = 0;
int proIndex = 0;
while(1){
//有空位便生产,没空位便阻塞等消费者消费
sem_wait(&sem_pro);
data = rand()%1234;
arr[proIndex] = data;
printf("product done %d\n",data);
proIndex = (proIndex+1)%SIZE;
//供消费者消费的数据加1
sem_post(&sem_con);
}
}
//消费者,只要环形队列中有数据,就不断消费
void*consumer(void*arg){
int data = 0;
int conIndex = 0;
while(1){
//环形队列中存在数据则消费,不存在数据则阻塞,直到有数据为止
sem_wait(&sem_con);
data = arr[conIndex];
printf("consume done %d\n",data);
conIndex = (conIndex+1)%SIZE;
//最后,消费了一个数据,空位加1
sem_post(&sem_pro);
}
}
int main(){
pthread_t pro,con;
sem_init(&sem_pro,0,SIZE-1); //一开始有很多空位置
sem_init(&sem_con,0,0); //但并没有数据
pthread_create(&pro,NULL,productor,NULL);
pthread_create(&con,NULL,consumer,NULL);
pthread_join(pro,NULL);
pthread_join(con,NULL);
sem_destroy(&sem_pro);
sem_destroy(&sem_con);
return 0;
}
以上实现的都是单消费者单生产者mo