线程基本API使用指北

总结一下线程的基本概念以及线程的创建、销毁、等待、分离

线程程序设计的引入进一步提高了程序的执行性能
本文通过对进程与线程及POSIX部分接口的相互关系的介绍,浅析操作系统中线程的概念。

线程是一个进程内部的执行序列,可以先从概念与进程简单对比

  • 进程是资源执行的最小单位
  • 线程是执行流的最小单位

线程的数据结构

  • 类似进程的PCB结构,线程的维护通过TCB结构。但是实际上linux基于PCB模拟TCB
  • 线程基于进程的mm_struct,拥有独立上下文私有栈空间,但仍共享代码段、数据段,共享堆空间,还包括
    • 文件描述符表
    • 信号的处理方式
    • 当前工作目录
    • 用户id/ 组id

线程的特性

  • 线程的优势
    • 线程创建的开销(空间、时间)比进程小的多
    • 线程切换的代价比进程小的多
    • 可以通过分解线程到不同处理器提升计算密集型作业的性能
    • 可以通过重叠I/O提升I/O密集型作业性能,线程可以等待不同的I/O操作
  • 线程的劣势
    • 密集型作业中线程数 > 处理器数,额外的线程切换带来了额外的同步、调度的开销
    • 线程缺乏保护,需要注意不该共享的变量等,使得健壮性降低
    • 更复杂的线程间调度进一步增加了构建代码以及调试程序的难度

线程ID

被称为线程ID的有两个

  • 第一个 pthread_t类型,此类型仅仅用来进程内部区分个线程,属于NPTL实现的层面
    (在Linux中,线程实现是Native POSIX Thread Libaray,简称NPTL)
  • 第二个 pid_t类型,是操作系统对线程的唯一标识符,属于内核进程调度的层面

后者即为 ps -elf看到的 LWP

1
2
3
4
ps -eLf |head -1 && ps -eLf |grep a.out |grep -v grep`
UID PID PPID LWP C NLWP STIME TTY TIME CMD
root 27243 23337 27243 0 2 11:22 pts/0 00:00:00 ./a.out
root 27243 22337 27244 0 2 11:22 pts/0 00:00:00 ./a.out

可以看到两行数据中 pid 是相同的
因为线程属于同一个进程,所以调用gitpid时,都会返回同一个进程pid
因为pid相同,只能用另一个标识区分内核下不同线程,即pid_t类型的线程ID

每个线程拥有独立的task_struct,从task_struct对pid与线程ID得到更清晰的理解

1
2
3
4
5
6
7
8
 struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
struct list_head thread_group;
};
  • pid 即进程id
  • tgid Thread Group ID 就是对应LWP的线程ID
  • 进程内所有线程属于一个线程组,第一个被创建的线程(即原来单进程单线程对应的线程)为主线程,该线程ID = 线程组 ID
  • NLWP 即为线程组内线程的数量

线程的创建与销毁

POSIX(可移植作业系统接口)标准规定了一系列对线程的操作函数(用户级函数库)
(使用makefile时要使用编译器命令的“-lpthread”)

以下是一个创建线程的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//API介绍见下文
#include <stdio.h>
#include <pthread.h>

void *myThread(void *arg)
{
print("This is %sst thread\n \
My Thread ID is %d\n", (char*)arg, phread_self() );

return (void*)1;

//exit(111); 调用此语句会使得整个进程退出
//pthread_exit( (void*)1 ); 线程退出函数(主线程不能使用,必须用进程的方式终止)
}

int main()
{
print("This is %sst thread\n \
My Thread ID is %d\n", "1", phread_self() );

prhtread_t tid; //tid的初始化由pthread_create完成
pthread_create(&tid, NULL, myThread, "2");

void *ret;
pthread_join(tid, &ret); //ret接收线程函数返回值


printf("main thread has created a new thread,it‘s result is &d\n", (int)ret);

return 0;

其中

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*star t_routine)(void*), void *arg);
    • 用来创建一个线程
    • thread:返回线程ID
    • attr:线程属性,NULL表⽰使⽤用默认属性
    • start_routine:线程启动后执行的函数的地址
    • arg:线程启动的参数
    • 成功返回0,失败返回错误码(其他函数一般返回-1,并设置错误码),也可以读取线程内错误码,但接受返回值的开销少一些
  • int pthread_join(pthread_t thread, void **value_ptr);
    • 阻塞的方式等待线程结束,如果不做此操作,则将遇到类似「僵尸进程」的问题,即资源泄漏
    • &tidprhtread_t类型的线程ID
    • void **用来接受线程函数返回值
    • 类似pthread_createpthread_join成功返回0,失败放回错误码
  • pthread_t pthread_self(void);
    • 获取当前线程的用户级线程ID
  • int pthread_cancel(pthread_t thread);
    • 取消一个执行中的进程

上文中的pthread_join虽然达到了避免资源泄漏的问题,但是会让主线程进入阻塞态
在我们不关心返回值的时候,我们还可以通过分离线程同样避免资源泄漏的问题
用「分离线程」替代「等待线程」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


void *myThread(void *arg)
{
sleep(1); //如果sleep加在main函数detach语句前,那么线程取消时线程可能已经执行了return

printf("haha");
return (void*)1;
}

int main()
{
prhtread_t tid;
pthread_create(&tid, NULL, myThread, "2");

void *ret;
pthread_join(tid, &ret); //ret接收线程函数返回值
pthread_detach(tid);
return 0;

其中

  • int pthread_detach(pthread_t thread);
    • 分离一个线程,可以不用阻塞主线程达到线程结束自动归还资源的目的(但无法接受返回值)
    • 可以自己分离自己,也可以由主线程分离
    • 分离的线程还依赖进程资源,发生异常依旧会引起 core dumped