简述概念与原理
想要编程来让linux系统“同时”执行多个任务,一种方法是使用多进程,也就是我在执行一个程序,哎,我又开一个程序,操作系统你让它俩“同时”运行;这种方式可以,但进程间的切换开销大,通信和同步较为复杂,不合适;我想实现的是,就开一个程序,这个程序的不同代码段可以“同时”运行;这就要用到多线程;
1 啥是线程?
线程是轻量级的进程;
进程是程序的一次动态执行,但是真正执行代码的是线程;操作系统为进程分配资源,进程中的线程来执行任务代码;一个进程至少包含一个线程,即主线程;其他线程(代码创建出来的)为子线程;
注:线程库不在linux内核中,使用线程需要在编译时链接;
进程与线程:
- 每个用户进程有自己的地址空间,系统为每个用户进程创建一个task_struct来描述该进程,该结构体中包含了一个指针指向该进程的虚拟地址空间映射表;
实际上task_struct 和地址空间映射表一起用来表示一个进程 - 由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大,
- 在同一个进程中创建的线程共享该进程的地址空间
- Linux里同样用task_struct来描述一个线程。线程和进程都参与统一的调度
- 线程是系统调度的最小单位
通常线程指的是共享相同地址空间的多个任务
使用多线程的好处: 大大提高了任务切换的效率;避免了额外的TLB & cache的刷新;
进程:
线程:
2 线程私有的资源
线程在进程中,那么多个线程就会共享这个进程的资源,同时,线程有自己的私有资源;*可以这样辅助理解:线程看上去就像一个子函数一样,所以私有栈和局部变量,有自己的ID等,而且子函数可以访问全局的所有东西; *
线程私有:
- 线程ID即TID;
- PC(程序计数器)和相关寄存器
- 堆栈:局部变量和返回地址
- 错误号 (errno)
- 信号掩码和优先级
- 执行状态和属性
线程共享:
- 可执行的指令,或者说程序的代码;
- 地址空间
- 静态数据(全局和静态)
- 进程中打开的文件描述符
- 当前工作目录
- 用户ID
- 用户组ID
一 线程的创建
1 线程句柄 / 线程描述符
要创建线程,首先先要创建一个句柄,线程的句柄或者说linux下的描述符,是pthread_t类型的变量;
(句柄这个词,翻译得简直糟糕透顶,初学者迷惑利器,但是理解了之后反而好用的很,但也不是翻译的功劳,而是这个词代表的本来含义很好用,再说一次,翻译得糟糕透顶)
pthread_t myThread1;
2 创建线程使用库函数pthread_create()
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
2.1 四个形参分别为:
(1) 线程句柄;
(2) 一个结构体指针,用于设置线程的属性,一般使用默认属性,传入NULL即可;(属性将在后面专门描述)
(3) 一个函数指针,用于把线程对应的子函数传入;(线程执行传入的子函数);
(4) 一个void指针,用来给线程传参,注意第三个形参的函数形参就是void*,就是往那里传,不传参写NULL;
2.2 返回值:
如果成功创建线程,pthread_create() 函数返回数字 0,反之返回非零值。各个非零值都对应着不同的宏,指明创建失败的原因,常见的宏有以下几种:(定义在errno.h文件中)
EAGAIN:系统资源不足,无法提供创建线程所需的资源。
EINVAL:传递给 pthread_create() 函数的 attr 参数无效。
EPERM:传递给 pthread_create() 函数的 attr 参数中,某些属性的设置为非法操作,程序没有相关的设置权限。
2.3 常用的两种形式:
(1) 一般来说,我们不需要改线程属性,也不需要给线程传参,那么创建很简单:pthread_create(句柄指针, NULL, 子函数名, NULL);
(2) 如果需要传参:pthread_create(句柄指针, NULL, 子函数名, (void*)参数指针);
看段代码:
为啥要加sleep(),为了让主线程停下来,好让子线程执行,否则,主线程可不管你子线程,主线程很快执行完直接就结束整个进程,所有线程都会被强行终止,直接没有现象;
1 #include <stdio.h>
2 #include <unistd.h> //调用 sleep() 函数
3 #include <pthread.h> //调用 pthread_create() 函数
4
5 void *ThreadFun(void *arg)//一个普通子函数,但是格式void*(函数名)(void*),可以往pthread_create()里传;
6 {
7 if (arg == NULL) {
8 printf("arg is NULL\n");
9 }
10 else {
11 printf("%s\n", (char*)arg);//对(void *)不能直接解引用,要转换成其他类型指针;
12 }
13 return NULL;
14 }
15
16 int main()
17 {
18 int res;
19 char * str = "hello";
20 //定义两个表示线程的变量(标识符)
21 pthread_t myThread1,myThread2;
22 //创建 myThread1 线程
23 res = pthread_create(&myThread1, NULL, ThreadFun, NULL);
24 if (res != 0)
25 {
26 printf("线程创建失败");
27 return 0;
28 }
29 sleep(5); //令主线程等到 myThread1 线程执行完成
30
31 //创建 myThread2 线程
32 res = pthread_create(&myThread2, NULL, ThreadFun,(void*)str);
33 if (res != 0)
34 {
35 printf("线程创建失败");
36 return 0;
37 }
38 sleep(5); // 令主线程等到 mythread2 线程执行完成
39 return 0;
40 }
bai@c:~/test$ gcc test.c -lpthread
bai@c:~/test$ ./a.out
arg is NULL //打印完后会等待5s;
hello //打印完后等待5s
bai@c:~/test$
很有用的函数:
#include <pthread.h>
pthread_t pthread_self(void);
可以返回当前线程的ID号,可以在线程中执行打印等操作;
二 线程的终止
1 终止方法
多线程程序中,终止线程执行的方式有 3 种,分别是:
- 线程执行完成后,自行终止;
- 线程执行过程中遇到了 pthread_exit() 或者 return,也会终止执行;
- 线程执行过程中,接收到其它线程发送的“终止执行”的信号,然后终止执行。
1.1 线程内终止:pthread_exit()和return
1.1.1 pthread_exit()函数和return都能终止函数和返回指定值;区别是return用于结束任意一个函数,pthread_exit()用于终止一个线程,在main()里非线程子函数的任意地方,包括调用的普通子函数内只要出现pthread_exit()就会终止主线程;导致后续代码无法执行;
#include <pthread.h>
void pthread_exit(void *retval);
pthread_exit()本身不返回值,但可以用一个指针向其他线程传递值;
1 #include <stdio.h>
3 #include <pthread.h>
15 void test()
16 {
17 printf("test\n");
18 pthread_exit(NULL);
19 }
20 int main()