十二、线程介绍
相关链接:线程
一、概述
1、为什么使用线程
从进程演化出线程,最主要的目的就是减少进程上下文切换开销。
进程是资源管理的最小单位,那么每个进程 都拥有自己的数据段、代码段和堆栈段。 这必然就造成了进程在进行切换时都需要有比较复杂的上下文切换等动作,因为要保存当前进程上下文的内容, 还要恢复另一个进程的上下文,如果是经常切换进程的话,这样子的开销就过于庞大,因为在进程切换上下文时, 需要重新映射虚拟地址空间、进出OS内核、寄存器切换,还会干扰处理器的缓存机制, 因此为了进一步减少CPU在进程切换时的额外开销,因此Linux进程演化出了另一个概念——线程。
线程是操作系统能够调度和执行的基本单位,在Linux中也被称之为轻量级进程。在Linux系统中, 一个进程至少需要一个线程作为它的指令执行体,进程管理着资源(比如cpu、内存、文件等等), 而将线程分配到某个cpu上执行。 一个进程可以拥有多个线程,它还可以同时使用多个cpu来执行各个线程 , 以达到最大程度的并行,提高工作的效率;同时,即使是在单cpu的机器上,也依然可以采用多线程模型来设计程序, 使设计更简洁、功能更完备,程序的执行效率也更高。
从上面的这些概念我们不难得出一个非常重要的结论:
线程的本质是一个进程内部的一个控制序列,它是进程里面的东西,一个进程可以拥有一个进程或者多个进程。
它们的关系就如图所示:
回顾一下进程相关的知识:
-
当进程执行fork()函数创建一个进程时,将创建出该进程的一份新副本。 这个新进程拥有自己的变量和自己的PID,它的执行几乎完全独立于父进程,这样子得到一个新的进程开销是非常大的。
-
而当在进程中创建一个新线程时,新的执行线程将拥有自己的栈,但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。 也就是说,它只使用当前进程的资源,而不是产生当前进程的副本。
-
Linux系统中的每个进程都有独立的地址空间,一个进程崩溃后,在系统的保护模式下并不会对系统中其它进程产生影响,
-
而线程只是一个进程内部的一个控制序列,当进程崩溃后,线程也随之崩溃,所以一个多进程的程序要比多线程的程序健壮, 但在进程切换时,耗费资源较大,效率要差一些。但在某些场合下对于一些要求同时进行并且又要共享某些变量的并发操作, 只能用线程,不能用进程。
2、特点
-
一个程序至少有一个进程,一个进程至少有一个线程。
-
线程使用的资源是进程的资源,进程崩溃线程也随之崩溃。
-
线程的上下文切换,要比进程更加快速,因为本质上,线程很多资源都是共享进程的,所以切换时, 需要保存和切换的项是很少的。
二、线程的调度策略
线程属性里包含了调度策略配置,POSIX 标准指定了三种调度策略:
-
分时调度策略,SCHED_OTHER。这是线程属性的默认值,另外两种调度方式只能用于以超级用户权限运行的进程, 因为它们都具备实时调度的功能,但在行为上略有区别。
-
优先级调度策略,先进先出方式调度(SCHED_FIFO)。基于队列的调度程序,对于每个优先级都会使用不同的队列, 先进入队列的线程能优先得到运行,线程会一直占用CPU,直到有更高优先级任务到达或自己主动放弃CPU使用权。
-
时间片轮转调度策略 ,时间片轮转方式调度(SCHED_RR)。与 FIFO相似,不同的是前者的每个线程都有一个执行时间配额, 当采用SHCED_RR策略的线程的时间片用完,系统将重新分配时间片, 并将该线程置于就绪队列尾,并且切换线程,放在队列尾保证了所有具有相同优先级的RR线程的调度公平。
与调度相关的API接口如下:
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
若函数调用成功返回0,否则返回对应的错误代码。
参数说明:
-
attr:指向一个线程属性的指针。
-
inheritsched:线程是否继承调度属性,可选值分别为
-
PTHREAD_INHERIT_SCHED:调度属性将继承于创建的线程,attr中设置的调度属性将被忽略。
-
PTHREAD_EXPLICIT_SCHED:调度属性将被设置为attr中指定的属性值。
-
policy:可选值为线程的三种调度策略,SCHED_OTHER、SCHED_FIFO、SCHED_RR。
三、线程栈
线程栈是非常重要的资源,它可以存放函数形参、局部变量、线程切换现场寄存器等数据, 在前文我们也说过了,线程使用的是进程的内存空间,那么一个进程有n个线程,默认的线程栈大小是1M, 那么就有可能导致进程的内存空间是不够的,因此在有多线程的情况下,我们可以适当减小某些线程栈的大小, 防止进程的内存空间不足,而某些线程可能需要完成很大量的工作,或者线程调用的函数会分配很大的局部变量, 亦或是函数调用层次很深时,需要的栈空间可能会很大,那么也可以增大线程栈的大小。
设置、获取线程栈大小可以使用以下函数:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
参数说明:
-
attr:指向一个线程属性的指针。
-
stacksize:线程栈的大小。
四、线程实验
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
/*要执行的线程*/
void *test_thread(void *arg)
{
int num = (unsigned long long)arg; /** sizeof(void*) == 8 and sizeof(int) == 4 (64 bits) */
printf("This is test thread, arg is %d\n", num);
sleep(5);
/*退出线程*/
pthread_exit(NULL);
}
int main(void)
{
pthread_t thread;
void *thread_return;
int arg = 520;
int res;
printf("start create thread\n");
/*创建线程,线程为test_thread函数*/
res = pthread_create(&thread, NULL, test_thread, (void*)(unsigned long long)(arg));
if(res != 0)
{
printf("create thread fail\n");
exit(res);
}
printf("create treads success\n");
printf("waiting for threads to finish...\n");
/*等待线程终止*/
res = pthread_join(thread, &thread_return);
if(res != 0)
{
printf("thread exit fail\n");
exit(res);
}
printf("thread exit ok\n");
return 0;
}
代码的分析如下:
-
第8~16行,定义test_thread函数作为线程要执行的函数,函数内部的操作是打印传入的arg参数, 然后睡眠一定的时间,最后调用thread_exit退出线程。
-
第29行,调用pthread_create函数创建线程,传入的线程函数指针为test_thread, 并且传入了一个函数参数arg(520),创建后线程将会开始执行test_thread的代码。
-
第40行,创建线程后调用 pthread_join等待线程退出。
要注意的是,本示例中需要在Makefile中添加lpthread链接库的内容:
LINK = -lpthread