Linux——线程深度剖析
1.并发和并行
- 并发 :在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥。并发是指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上有多个进程被同时执行的效果–宏观上并行,针对单核处理器
- 并行 :在单处理器中多道程序设计系统中,进程被交替执行,表现出一种并发的外部特种;在多处理器系统中,进程不仅可以交替执行,而且可以重叠执行。在多处理器上的程序才可实现并行处理。从而可知,并行是针对多处理器而言的。并行是同时发生的多个并发事件,具有并发的含义,但并发不一定并行,也亦是说并发事件之间不一定要同一时刻发生。(同一时刻,有多条指令在多个处理器上同时执行–针对多核处理器)
2.同步和异步
- 同步:进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。
-
异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
3.线程概念
3.1什么是线程?
- 在Linux环境下,线程就是轻量级的进程(LWP),本质仍是进程,而我们通俗所说的线程是C库中的概念
- 线程分为主线程和工作线程,每个线程都拥有自己的task_struct结构体,但其中的内存指针指向同一块虚拟地址空间,没有独立的地址空间(共享)。
- 线程:最小的执行单位,调度的基本单位
- 进程:最小分配资源单位,可看成是只有一个线程的进程。
getpid()
得到的是进程的pid,在内核中,每个线程都有自己的PID,要得到线程的PID,必须用syscall(SYS_gettid)
;pthread_self
函数获取的是线程标识符ID,线程ID在某进程中是唯一的,在不同的进程中创建的线程可能出现ID值相同的情况。是该线程在共享区中开辟空间的对应首地址
3.2Linux内核线程实现原理
- 对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
- 线程不同,两个线程具有各自独立的PCB,但内存指针指向同一个虚拟地址空间,共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
- 每个线程在共享区都会开辟一段属于自己的空间,存放:
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
5.信号屏蔽字
6.调度优先级 - 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
- 无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
4.线程的优缺点
- 优点:1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
- 缺点:1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
5.线程的操作
5.1获取线程ID。
其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void);
-
pthread_t:typedef unsigned long int pthread_t;
-
返回值:成功:0; 失败:无!
-
线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
-
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
5.2创建一个线程。
其作用,对应进程中fork() 函数。( 链接这些线程函数库时要使用编译器命令的“-lpthread”选项)
`int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);`
- 返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号。
- 参数1:出参,保存系统为我们分配好的线程ID
- 参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
- 参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
- 参数4:线程主函数执行期间所使用的参数,但注意,传参不能传临时变量,因为临时变量在离开其作用域后空间就会被销毁,最好传入在堆上开辟的空间。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void* func(void* arg)
{
while(1)
{
printf("i am work thread\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, TStart, NULL);
if(ret < 0)
{
perror("wrong!");
return -1;
}
while(1)
{
printf("i am main thread\n");
sleep(1);
}
return 0;
}
主进程结束后,所有的工作线程都会结束。该进程的进程pid=3472
pstack 3472
查看该进程的堆栈信息:
- 【LWP:3472】:主线程PID,类型是pid_t,与进程PID3472相同
- 【LWP:3473】:工作线程PID,类型是pid_t
- 【Thread 0x7fdfc61af740】:主线程ID,类型是thread_t,是一段地址(64位),是该线程在共享区中开辟空间的对应首地址(图解的粉色框框)
- 【Thread 0x7fdfc59bf700】:工作线程ID,类型是thread_t,是一段地址(64位),同上。
5.3线程间共享全局变量
线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助mmap
1 #include <stdio.h>
2 #include <signal.h>
3 #include <unistd.h>
4 #include <stdlib.h>
5 #include <sys/wait.h>
6 #include <pthread.h>
7 int var=100;
8 void* TStart(void* arg)
9 {
10 var = 200;
11 printf("thread\n");
12 return NULL;
13 }
14
15 int main()
16 {
17 printf("At first var = %d\n", var);
18
19 pthread_t tid;
20 pthread_create(&tid, NULL, TStart, NULL);
21 sleep(1);
22
23 printf("after pthread_create, var = %d\n", var);
24
25 return 0;
26 }
5.4线程终止
- 从入口函数的return返回,线程退出
void pthread_exit(void *retval);
参数:返回信息,返回给等待线程退出的执行流信息,可传递NULL,谁调用谁退出int pthread_cance1 (pthread_t thread);
thread:线程标识符,调用该函数的执行流可以取消其他线程,但是需要知道其他线程的线程标识符也可以执行流自己取消自己,传入自己的线程标识符int pthread_cancel(pthread_t thread);
thread:线程标识符,调用该函数的执行流可以取消其他线程
注意:
1.线程在创建出来的时候,属性当中默认是joinable属性(意味着线程在退出的时候需要其他执行流来回收线程的资源)
2.使用exit将指定线程退出,可以吗? 结论:线程中,禁止使用exit函数,会导致进程内所有线程全部退出。
5.5线程等待
获取线程退出状态 其作用,对应进程中 waitpid() 函数
int pthread_join(pthread_t thread, void **retval);
- 参数1 :线程ID
- 参数2:存储线程结束状态,
1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数 - 成功:0;失败:错误号
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREADCOUNT 1
void* myStartThread(void* arg)
{
(void)arg;
while(1)
{
sleep(5);
printf("i am workthread\n");
pthread_exit(NULL);
}
}
int main()
{
pthread_t tid[THREADCOUNT];
for(int i = 0; i < THREADCOUNT; i++)
{
int ret = pthread_create(&tid[i], NULL, myStartThread, NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0; i < THREADCOUNT; i++)
{
pthread_join(tid[i], NULL);
}
while(1)
{
printf("i am main thread\n");
sleep(1);
}
return 0;
}
5.6线程分离
int pthread_detach(pthread_t thread);
改变线程的属性,将joinable属性改变成为detach属性,当线程退出的时候,不需要其他线程在来回收退出线程的资源,操作系统会默认回收掉
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREADCOUNT 1
void* myStartThread(void* arg)
{
//1.pthread_detach(pthread_self());
(void)arg;
while(1)
{
sleep(5);
printf("i am workthread\n");
}
}
int main()
{
pthread_t tid[THREADCOUNT];
for(int i = 0; i < 1; i++)
{
int ret = pthread_create(&tid[i], NULL, myStartThread, NULL);
if(ret < 0)
{
perror("pthread_create");
return -1;
}
}
for(int i = 0; i < THREADCOUNT; i++)
{
//2.pthread_detach(tid[i]);
}
while(1)
{
printf("i am main thread\n");
sleep(1);
}
return 0;
}
可以放在工作线程之中,刚进入工作线程就进行分离,也可以在主线程中根据工作线程ID进程分离。