线程(thread)是一个程序里的执行路线,每个进程都至少有一个线程。
线程基础概念
什么是线程
线程(thread)是一个程序里的执行路线,每个进程都至少有一个线程。在单线程下,一个进程同一时刻只能干一件事情,而在多线程进程中,同一时刻可以有多个执行流程。如下图:
线程的特点
多线程共享的部分:
- 线程是操作系统调度的最小单位,也是进程中实际运作的单位;
- 它是单一顺序的控制流,可以并发执行;
- 多个线程共享进程的代码段、数据段、文件描述符表、信号的处理方式、当前工作目录、用户ID和组ID。
线程独立的部分:
- 线程 ID;
- 一组寄存器;
- 栈空间;
- errno;
- 信号屏蔽字;
- 调度优先级。
线程优点:
- 线程的创建和销毁相对于进程来说代价要小的多;
- 线程之间的切换比进程间的切换方便的多;
- 线程占用的资源相比于进程更少;
- 能充分利用多核处理器资源;
- 在等待慢速 I/O 的时候,程序可以执行其他任务;
- 创建多个线程执行计算(I/O)密集型业务,能提高效率。
线程缺点:
- 缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会影响整个进程;
- 健壮性降低:编写多线程需要更全面深入的考虑,在多线程程序里,由于时间分配上细小的差别或者资源分配上的不合理可能会造成对整个进程的不良影响;
- 当线程的数量大于处理器核心的数量时,线程调度所带来的开销会造成较大的性能损失;
- 编程难度高,编写与调试一个多线程进程要比单线程进程困难。
线程创建
POSIX线程库
POSIX线程一个线程的标准,定义了一套操作线程的 API。它以
pthread.h
和一个线程库实现,其中的大多数 API 都以pthread_
开头,链接该线程函数库时,要加上命令-lpthread
。
线程创建API:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- thread:输出型参数,成功的话返回新创建的线程 ID;
- attr:定制线程属性,NULL代表使用默认属性;
- 线程的入口函数地址,该函数又一个
void*
的参数;- arg是传给线程入口函数的参数,如果需要传多个参数,则需要将多个参数封装为一个结构体,在将该结构体指针传入。
返回值:成功返回0,失败返回错误码,但它并不会设置全局变量errno
,从函数中直接返回错误码更为清晰整洁,不需要依赖那些随着函数执行而不断变化的全局状态,这样可以把错误限制在引起错误的函数中。
下面是一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* EntryFunc(void *arg)
{
(void)arg;
while(1){
printf("myThread!\n");
sleep(1);
}
}
int main()
{
pthread_t id;
if(pthread_create(&id, NULL, EntryFunc, NULL) != 0){
exit(0);
}
while(1){
printf("main thread!\n");
sleep(1);
}
return 0;
}
运行结果:
线程标识
就像每个进程有一个 ID 一样,每个线程也有自己的 ID。进程的 ID 在整个操作系统内是唯一的,但线程不同,线程 ID 只有在它所属的进程的上下文才有意义。
线程 iD 是一个 pthread_t
的数据类型,这个数据类型在 ubuntu 16。04
下被定义为 无符号长整型,在其实现代码中可以看到这样一句:
在其它的操作系统下,该类型有不同的定义,因此我们需要用一个函数来比较两个线程的 ID。
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
如果两个线程 ID 相等,则返回一个非 0 的值,否则返回 0;
下面这个函数可以获取线程自身的 ID:
#include <pthread.h>
pthread_t pthread_t pthread_self(void);
现在我们重新审视一下之前的进程ID,在这里,进程个PCB 中描述的进程个 ID 实际上是线程 ID,也是 tgid
(Thread Group ID),对应用户层面的 ID。
该如何查看一个线程的 ID 呢?
依次在命令行输入如下指令:
ps -eLf | head -1
ps -elf | grep pthread | grep -v grep
其中
-L
选项显示线程ID和线程组内线程的个数
pthread是我们在另一个终端下启动的一个多线程进程
指令的结果:
可以看到:该线程组中有两个线程, 线程 ID 分别为:4637,4638,其中第一个线程是该进程的主线程,其 ID 和进程 ID 相等。
在进程中,有父进程,子进程的概念,而在线程中,所有线程都是对等的,没有上下级的关系。
下面的多线程进程的地址空间布局:
线程终止
如果进程中的任意一个线程调用了 exit
、_Exit
、_exit
,那么整个进程就会终止。如果想让某一个线程中止,有下面三种方式:
- 调用
return
返回到主线程;- 自己调用
pthread_exit
终止自己;- 同一个进程中的其他线程调用
pthread_cancel
来终止指定线程。
下面是两个函数的原型:
#include <pthread.h>
void pthread_exit(void *retval);
int pthread_cancel(pthread_t thread);
其中第一个函数的参数为返回给主线程的返回值,第二个函数的参数为想要终止的线程的 tid,该函数执行成功返回 0, 出错 返回一个 非0值。
线程等待与分离
线程等待
在进程中,子进程执行完毕后,父进程不进程 wait,就无法回收子进程占用的部分资源,线程中也是如此。已经退出的线程,其所占的内存空间没有被释放,而且新创建的线程也不会复用已退出的线程的空间。这样如果不进行线程等待的话,就会造成资源泄漏。
下面这个函数用于等待线程并回收其所占用的资源。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
其中
thread
为要等待的线程的 ID,retval
是一个输出型参数,保存线程的退出状态,如果不关心线程的退出状态可以将其设为 NULL。通过不同方式终止线程,其返回状态有所不同,如果线程通过return
返回,则retval
所指的空间是返回值,如果其它线程通过调用pthread_cancel
终止本线程,则返回状态为:
下面是一个例子:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- thread:输出型参数,成功的话返回新创建的线程 ID;
- attr:定制线程属性,NULL代表使用默认属性;
- 线程的入口函数地址,该函数又一个
void*
的参数;- arg是传给线程入口函数的参数,如果需要传多个参数,则需要将多个参数封装为一个结构体,在将该结构体指针传入。
返回值:成功返回0,失败返回错误码,但它并不会设置全局变量errno
,从函数中直接返回错误码更为清晰整洁,不需要依赖那些随着函数执行而不断变化的全局状态,这样可以把错误限制在引起错误的函数中。
下面是一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
void* EntryFunc(void *arg)
{
(void)arg;
sleep(3);
int *ret = (int*)malloc(sizeof(int));
ret = 666;
return (void*)ret;
}
int main()
{
pthread_t id;
if(pthread_create(&id, NULL, EntryFunc, NULL) != 0){
return 1;
}
void *ret;
pthread_join(id, &ret);
printf("%d\n", *(int*)ret);
return 0;
}
执行结果:
这种方式,调用线程需要阻塞式的等待线程退出,如果不关心线程的退出状态的话,可以使用 pthread_detach
来分离线程,告诉线程在退出时,自动释放资源。
进程蔚线程对比
进程原语 | 线程原语 | 描述 |
---|---|---|
fork | pthread_create | 创建新的控制流 |
exit | pthread_exit | 从现有控制流中退出 |
waitpid | pthread_join | 获取控制流的退出状态 |
getpid | pthread_self | 或控制流的 ID |
abort | pthread_cancel | 非正常退出控制流 |
默认情况下,线程的终止状态会保存直到对该线程调用 pthread_join
,如果线程已经进城分离,则线程的资源在线程终止时like被收回。不能对线程即等待又分离,比如已经对一个线程调用 pthread_detach
分离了,就不能再调用 pthread_join
,因为对分离线程调用pthread_join
会产生未定义行为。
——完!
【作者:果冻:https://blog.csdn.net/jelly_9】