线程的概念:
1. 什么是线程?
百度百科给出的定义太长了,总结的来说:一个线程就是一个执行流。
我们了解到程序运行后,就会变成进程,而一个进程中执行的路线就称为一个执行流(线程),更明确的定义是:线程是一个进程内部的控制序列。
因为Linux中没有线程的概念,所以Linux下的线程称为“轻量级进程”。
2. 线程和进程的关系
进程是内存资源分配的最小单位,
线程是调动的最小单位。
图解:
由此图我们了解到,一个程序中有多个函数,假设每个函数执行时间为1,那么整个程序的执行时间是5。而我们如果引入线程的概念,实每个线程负责一个函数,那我们最终的执行时间为2。这样就大大减少了程序的时间,提高了效率。(当然这是理想状态,其中还涉及到线程的切换什么,调度算法什么。)总的来说,引入线程,会处理许多进程不方便完成的事情。
我们知道进程中的创建子进程,资源共享,数据写时拷贝。那线程也一样吗?
我们从线程的概念中了解到:
- 有一部分资源共享(数据和资源,文件描述符表,当前的工作目录,用户id和组id)
- 有属于自己的独立资源(线程ID,寄存器,私有栈,信号屏蔽字,调度优先级(上下文))
线程的优缺点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,线程不是一个可执行的实体。
线程控制:
因为Linux中没有线程的概念,所以对应的就没有了线程的函数和库。所以我们就得引入POSIX线程库。
线程的创建:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
//thread:返回线程id
//attr:设置线程属性
//start_routine:线程创建完成时所执行的函数
//arg:传给线程执行函数的参数
//返回值:0为成功,失败返回错误码
下面来验证一下
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *fun()
{
while(1)
{
printf("|||||||||||||||||||||||\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,fun,NULL);
if(ret != 0)
{
printf("prhread_create error!\n");
exit(-1);
}
while(1)
{
printf("------------------------\n");
sleep(1);
}
return 0;
}
在上面的代码中我们清楚的看到有俩个函数,每个函数中都有一个死循环,并且main函数中没有调用这个函数。按照正常来推断,我们最终得到是main函数内的死循环。可是结果呢?
从实验效果我们可以推测到,这个创建线程一定是在进程内部增加了一条执行流。分别执行各自的代码片段。
进程和线程的ID:
- 在Linux中因为没有线程的概念,所以Linux中关于线程的描述是“轻量级进程”,而在操作系统眼中,对于线程的描述和组织也是采用:task_struct,意味着线程也有自己的进程描述符。
- 没有线程以前,一个进程对应一个进程描述符,即使创建子进程也是如此。但是引入了线程的概念之后,我们明白线程也是需要拥有自己的进程描述符。那么在内核的眼中一个进程中对应N个进程描述符,在控制中不会产生错误吗?
这是我们上面测试的例子,执行中我们查看当前系统的线程状态,发现他们俩的PID相同,不同的只是LWP。那么LWP是什么呢? 是线程ID,即gettid()的返回值。
那么在一个进程中拥有多个线程的进程称为什么呢? 这就引入了多线程的概念:线程组。
struct task_struct
{
.
.
.
int pdeath_signal; //父进程终止是向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid;
==========
//进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
===========
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
.
.
.
};
从上面的结构体中来看,有pid_t pid , pid_t tgid俩个 ,那么这和线程又有什么关系呢? 其中pid呢代表的是每个独立的线程的线程id,而tgid呢是指在用户层面上的进程ID,相当于一个线程组中tgid相等(相当于这个进程的id),而pid呢是每个线程的id。
获取线程ID:
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);
线程终止:
- 线程的调用函数中执行return。但是在主线程中不适用这种,主线程return相当于进程退出
- 线程可以自己调用pthread_exit来结束自己
- 其他进程可以调用pthread_cancel来结束其他的进程
#include <pthread.h>
void pthread_exit(void *retval);
//无返回值
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//返回值:成功为0,失败返回错误码
线程的等待与分离 :
我们都知道进程需要等待,如果不等待的话,就会造成僵尸进程,从而引发内存泄露。所以线程也必须要进行等待。
int pthread_join(pthread_t thread,void **value_prt);
//thread:线程ID
//value_prt:指向一个指针,指针指向线程的而返回值
//返回值:成功为0,失败返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
void *fun()
{
int i = 5;
pid_t tid;
tid = syscall(SYS_gettid);
while(i--)
{
printf("|||%d\n",tid);
sleep(1);
}
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid,NULL,fun,NULL);
void *rt;
if(ret != 0)
{
printf("prhread_create error!\n");
exit(-1);
}
pthread_join(tid,&rt);
printf("新线程退出\n");
return 0;
}
线程的分离:
默认情况下,线程是需要join的,意思是线程退出后,必须要进行pthread_join操作,如果不进行join操作,线程就无法释放其资源,造成内存泄露。
但是当我们不关心一个线程的返回值,join是一种浪费。所以我们将该线程从线程组分离出去,相当于告诉系统,这个线程退出直接释放线程资源,不需要等待。
int pthread_detach(pthread_t thread);
一个线程可以是其他线程使其分离,也可以是自己使自己分离。
joinable和分离是冲突,一个线程不能既joinable,又分离。
测试代码:
void* fun()
{
//pthread_detach(pthread_self());
printf("111\n");
sleep(1);
printf("xiancheng quit!\n");
return NULL;
}
int main()
{
int i = 0;
while(1)
{
i++;
pthread_t tid;
int ret = pthread_create(&tid,NULL,fun,NULL);
if(ret != 0)
{
printf("exit :%d i = %d\n",ret,i);
return 0;
}
usleep(99);
}
return 0;
}
我们先来测试一下没有调pthread_join函数无限创建新线程。来看下效果:
看明白了吗? 如果线程没有调用pthread_join函数,那么退出的新线程的资源就不会回收,而我们可以清晰地看到,整个代码执行过程中我们共创建了32755个新线程,在创建32766个线程发现内存空间不足了,所以返回创建线程失败的错误码。
那我们要是每创建一个线程,就调用pthread_detach函数会是什么效果呢?
我们可以清楚的看到只是调用线程函数中进行了线程分离,而创建线程的个数就大大增加了。
这下应该能体会到了等待和分离的作用了吧!