Linux线程编程初步
一些历史背景
- Linux间接起源于Unix,而Linux诞生时并不存在 "线程"的概念。
- 在20世纪90年代线程才流行起来,POSIX Thread标准于 1995年确立。
- Unix中引入 Thread 之后,大量函数被重写,信号机制也变得复杂。
- 2005年之后,处理器生产厂商向超线程和多核架构靠拢。
- 超线程是英特尔开发出来的一项技术,使得单个处理器可以像两个逻辑处理器那样运行,这样单个处理器可以并行执行线程。
一些常见的概念
物理处理器:
安装在主机上的真实的处理器硬件。
逻辑处理器:
逻辑处理器与超线程技术相关。
不支持超线程:逻辑处理器的数量等于核心数的数量
支持超线程: 逻辑处理器的数量是处理器核心数的两倍
核心数:即多核处理器中的内核数量
通过工艺手段将多个完整的 CPU 塞进一个处理器封装中(每一个 CPU 就是一个核)
线程与进程的关系
进程:
应用程序的一次加载执行(系统进行资源分配的基本单位)
线程:
进程中的程序执行流
- 一个进程中可以存在多个线程(至少存在一个线程)
- 每个线程执行不同的任务(多个线程可并行执行)
- 同一个进程中的多个线程共享进程的系统资源
创建线程并不难,难的是创建的这些线程会和主线程一起往下执行。
初探线程编程模型
Linux多线程API函数
线程创建 :pthread_create
NAME
pthread_create - create a new thread
SYNOPSIS
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- pthread_t *thread :变量的地址,用于返回线程标识
- attr :线程的属性,可设置为NULL,即:使用默认属性
- start_routine :线程入口函数
- arg :线程入口函数参数
线程标识 :pthread_self
pthread_t pthread_self(void); 获取当前线程的 ID 标识
线程等待 :pthread_join
int pthread_join(pthread_t thread,void** retval);
等待目标线程执行结束
main.c
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void* thread_entry(void* arg)
{
pthread_t id = pthread_self();
int n = (long)arg;
int i =0;
while(i < n)
{
printf("id = %ld,i = %d\n",id,i);
sleep(1);
i++;
}
return NULL;
}
int main(int argc,char* argv[])
{
pthread_t t1 = 0;
pthread_t t2 = 0;
long arg1 = 10;
long arg2 = 5;
pthread_create(&t1,NULL,thread_entry,(void*)arg1);
pthread_create(&t2,NULL,thread_entry,(void*)arg2);
printf("t1 = %ld\n",t1);
printf("t2 = %ld\n",t2);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
return 0;
}
编译运行:
wj@ubuntu:~/DTThread/1-8$ gcc main.c -o main.out -lpthread
wj@ubuntu:~/DTThread/1-8$ ./main.out
t1 = 140185317775104
t2 = 140185309382400
id = 140185317775104,i = 0
id = 140185309382400,i = 0
id = 140185309382400,i = 1
id = 140185317775104,i = 1
id = 140185317775104,i = 2
id = 140185309382400,i = 2
id = 140185317775104,i = 3
id = 140185309382400,i = 3
id = 140185317775104,i = 4
id = 140185309382400,i = 4
id = 140185317775104,i = 5
id = 140185317775104,i = 6
id = 140185317775104,i = 7
id = 140185317775104,i = 8
id = 140185317775104,i = 9
对比实验
实验一:性能对比
相同功能的 多线程程序 vs 多进程程序的
对比项:创建/销毁
实验二:内存共享
多线程程序共享一段内存 -> "全局变量"
多进程程序共享一段内存 -> "机制复杂"
多线程/多进程创建销毁10000次对比
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <sched.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL
#define TEST_LOOPS 10000
#define DiffNS(begin,end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC \
+ (end.tv_nsec - begin.tv_nsec))
void* thread_entry(void* arg)
{
return NULL;
}
void thread_test()
{
printf("thread test:\n");
struct timespec begin = {0};
struct timespec end = {0};
pthread_t tid = 0;
int i = 0;
int diff = 0;
clock_gettime(CLOCK_MONOTONIC,&begin);
for(i=0;i<TEST_LOOPS;i++)
{
pthread_create(&tid,NULL,thread_entry,NULL);
pthread_join(tid,NULL);
}
clock_gettime(CLOCK_MONOTONIC,&end);
diff = DiffNS(begin,end) / NSECS_PER_MSEC;
printf("result = %dms\n",diff);
}
void process_test()
{
printf("process test: \n");
struct timespec begin = {0};
struct timespec end = {0};
pid_t pid =0;
int i=0;
int diff =0;
clock_gettime(CLOCK_MONOTONIC,&begin);
for(i=0;i<TEST_LOOPS;i++)
{
pid = fork();
if(pid) waitpid(pid,NULL,0);
else return ;
}
clock_gettime(CLOCK_MONOTONIC,&end);
diff = DiffNS(begin,end) / NSECS_PER_MSEC;
printf("result = %dms\n",diff);
}
int main()
{
thread_test();
process_test();
return 0;
}
编译运行:
wj@ubuntu:~/DTThread/1-7$ gcc test.c -o test.out -lpthread
wj@ubuntu:~/DTThread/1-7$ ./test.out
thread test:
result = 652ms
process test:
result = 1787ms
wj@ubuntu:~/DTThread/1-7$
可以看到多进程创建/销毁 更加耗时。
多线程/多进程创建销毁20000次对比
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <sched.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL
#define TEST_LOOPS 20000
#define DiffNS(begin,end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC \
+ (end.tv_nsec - begin.tv_nsec))
void* thread_entry(void* arg)
{
return NULL;
}
void thread_test()
{
printf("thread test:\n");
struct timespec begin = {0};
struct timespec end = {0};
pthread_t tid = 0;
int i = 0;
int diff = 0;
clock_gettime(CLOCK_MONOTONIC,&begin);
for(i=0;i<TEST_LOOPS;i++)
{
pthread_create(&tid,NULL,thread_entry,NULL);
pthread_join(tid,NULL);
}
clock_gettime(CLOCK_MONOTONIC,&end);
diff = DiffNS(begin,end) / NSECS_PER_MSEC;
printf("result = %dms\n",diff);
}
void process_test()
{
printf("process test: \n");
struct timespec begin = {0};
struct timespec end = {0};
pid_t pid =0;
int i=0;
int diff =0;
clock_gettime(CLOCK_MONOTONIC,&begin);
for(i=0;i<TEST_LOOPS;i++)
{
pid = fork();
if(pid) waitpid(pid,NULL,0);
else return ;
}
clock_gettime(CLOCK_MONOTONIC,&end);
diff = DiffNS(begin,end) / NSECS_PER_MSEC;
printf("result = %dms\n",diff);
}
int main()
{
thread_test();
process_test();
return 0;
}
编译运行:
wj@ubuntu:~/DTThread/1-7$ gcc test.c -o test.out -lpthread
wj@ubuntu:~/DTThread/1-7$ ./test.out
thread test:
result = 1260ms
process test:
result = 3511ms
可以看到多进程创建/销毁更加耗时。
多线程内存共享
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <sched.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <pthread.h>
void* thread_entry(void* arg)
{
printf("pid = %d,ppid = %d,pgid = %d\n",getpid(),getppid(),getpgrp());
char* shmaddr = arg;
strcpy(shmaddr,"D.T.Software");
return NULL;
}
int main()
{
char* mem = malloc(128);
pthread_t child = 0;
printf("mem = %p\n",mem);
if(mem == NULL)
{
printf("malloc error\n");
exit(1);
}
int r = pthread_create(&child,NULL,thread_entry,mem);
if(r == 0)
{
printf("pid = %d,ppid = %d,pgid = %d\n",getpid(),getppid(),getpgrp());
char* shmaddr = mem;
pthread_join(child,NULL);
printf("%s\n",shmaddr);
}
else
{
printf("create thread error...\n");
}
free(mem);
return 0;
}
编译运行:
wj@ubuntu:~/DTThread/1-7$ gcc shm-thread.c -o shm-thread.out -lpthread
wj@ubuntu:~/DTThread/1-7$ ./shm-thread.out
mem = 0x55ffc6ee82a0
pid = 76297,ppid = 4384,pgid = 76297
pid = 76297,ppid = 4384,pgid = 76297
D.T.Software
多进程内存共享
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <errno.h>
#include <sched.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/wait.h>
#include <sys/shm.h>
#define PATH_NAME "."
#define PROJ_ID 88
int main()
{
key_t k = ftok(PATH_NAME,PROJ_ID);
int shmid = shmget(k,128,IPC_CREAT | S_IRWXU);
printf("shmid = %d\n",shmid);
if(shmid == -1)
{
printf("shmget error\n");
exit(1);
}
int pid = fork();
if(pid)
{
printf("pid = %d,ppid = %d,pgid = %d\n",getpid(),getppid(),getpgrp());
char* shmaddr = shmat(shmid,NULL,0);
waitpid(pid,NULL,0);
printf("%s\n",shmaddr);
}
else if(pid == 0)
{
printf("pid = %d,ppid = %d,pgid = %d\n",getpid(),getppid(),getpgrp());
char* shmaddr = shmat(shmid,NULL,0);
strcpy(shmaddr,"D.T.SoftWare");
exit(0);
}
else
{
printf("fork error...\n");
}
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
编译运行:
wj@ubuntu:~/DTThread/1-7$ ./shm-proc.out
shmid = 65585
pid = 76855,ppid = 4384,pgid = 76855
pid = 76856,ppid = 76855,pgid = 76855
D.T.SoftWare
通过以上输出,可以看到是两个不同的进程,在共享同一块内存。
深入研究线程原理
Linux中线程本质
- 线程是后引入操作系统的概念。进程是最先引入操作系统的概念,然后是引入了线程,然后又引入了协程。从进程 -> 线程 -> 协程 来看,执行单元要越来越轻量化。
- 线程接口由 Native POSIX Thread Library提供,即:NPTL库函数。
- 线程被称为轻量级进程(Light Weighted Process)
- 每一个线程在内核中都对应一个调度实体,拥独立的结构体 (task_struct)
- 内核设计:一个进程对应内核中的一个结构体(task_struct),对应一个进程标识(PID)
- 引入线程:线程是内核的调度实体,在内核中必然对应一个结构体。
- 内核角度:对于一个线程来说,它与一个进程的区别在哪里?就是资源不一样。
Linux中的线程本质:
用户模式:一个进程中存在多个线程
内核模式:每个线程是一个独立的调度实体。
问题:内核怎么知道某个调度实体属于哪一个进程?
另外一种视角
- 拥有多线程的进程,又被称为线程组(谁是线程组长?,主线程是线程组长)
- 在内核数据结构 task_struct 中存在 pid 和 tgid :
- pid_t pid -> 线程标识符 (Thread ID)
- pid_t tgid -> 线程组标识符 (Thread Group ID),用来存储用户模式下的进程标识
- 对于主线程来说,pid 等于 tgid
用户模式 | 系统调用 | task_struct |
线程标识符 | pid_t gettid(void); | pid_t pid; |
进程标识符 | pid_t getpid(void); | pid_t tgid; |
进程创建后默认拥有一个线程,即:主线程(默认执行流)
主线程的LWP标识符与进程标识符相同,即:主线程为线程组长(pid等于tgid)
其他子线程创建后隶属于当前进程:
子线程的LWP标识符各不相同,且与进程标识符不同,但tgid相同
子线程调用getpid()的结果相同(why?)
实验demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/syscall.h>
//从内核的角度,拿到线程id,也就是内核 task_struct结构体中的 pid
pid_t gettid()
{
return syscall(SYS_gettid);
}
void* thread_enrty(void* arg)
{
pid_t pid = getpid(); //和 main函数中,主线程 pid 一样
pid_t tid = gettid(); //和 mian函数中,主线程 的 tid 不一样
pthread_t self = pthread_self();
printf("thread:pid = %d\n",pid);
printf("thread:tid = %d\n",tid);
printf("thread:self = %ld\n",self);
return NULL;
}
int main(int argc,char* argv[])
{
pthread_t t=0;
pthread_t self = pthread_self();
pid_t pid = getpid();
pid_t tid = gettid(); //主线程的 pid 等于 tgid
printf("main:pid = %d\n",pid);
printf("main:tid = %d\n",tid);
printf("main:self = %ld\n",self);
pthread_create(&t,NULL,thread_enrty,NULL);
printf("main:t = %ld\n",t);
pthread_join(t,NULL);
return 0;
}
编译输出:
wj@wj:~/linux/1-7$ gcc test1.c -o test1.out -lpthread
wj@wj:~/linux/1-7$ ./test1.out
main:pid = 10495
main:tid = 10495
main:self = 132228026910528
main:t = 132228021487296
thread:pid = 10495
thread:tid = 10496
thread:self = 132228021487296
wj@wj:~/linux/1-7$
思考问题
对于一个线程来说, pid_t tid = gettid(); pthread_t self = pthread_self(); 这两个函数都能够标识一个线程,那么他们之间的区别是什么呢?
线程 pid_t 是全局范围内对调度实体的唯一标识,是给内核层面使用的。
线程 pthread_t 是应用程序中对线程的局部描述,是用户层面使用的。
值得思考的问题
1.多线程之间是否有 “父子关系”?
无。进程中只有主线程和子线程,线程之间没有“父子关系”。
2.主线程如果先于子线程结束会发生什么?
Linux中主线程如果执行结束,则进程结束。进程结束则进程资源被释放,子线程被迫结束。
3.使用kill命令是否能够“杀死”制定线程?
默认情况下,kill是用来给进程发信号的(SIGTREM信号),而信号的目标是进程,是用来杀死进程的。
因此,kill任意子线程的 pid_t 将导致整个进程结束。
4.pthread_t 究竟是什么数据类型?
pthread_t是 POSIX Thread中的接口,具体定义与系统相关。
通常情况下,pthread_t的具体定义是一个无符号整型值。
Linux中 pthread_t的定义是64位整型(保存地址值)
其他系统中,pthread_t直接映射为 Task ID值
对于一些特殊的系统,pthread_t是一个结构体。