线程的概念
在生活中,我们在使用电脑时,可以同时使用多个软件,比如:在写代码的同时,还可以听着音乐.即使有一个CPU,也可以同时进行多个任务.
更充分的利用多核CPU:可以将一个任务分给几个CPU然后同时进行.
线程:是一个进程内部的”控制序列”.
任意进程至少有一个线程.
特点:
1.线程是操作系统进行调度的最小单位.
2.一条线程指的是一个单一顺序的控制流(一个进程中可以有多个控制流)
3.一个进程可以并发多个线程,每条线程并行执行不同的任务.
进程和线程的区别
1.资源所有权
进程(或者任务)用于管理和分配资源.
线程(或轻量级进程)是调度和执行的基本单位.(所以在Linux中,描述线程的也是PCB).
进程:也叫线程组.
线程:也可叫轻量级进程.
在一个进程中可以有多个线程,而每个线程都是平等的,只有一个主线程即main函数所在线程.在每个线程中,都有自己的调用栈和上下文,而虚拟内存中的栈是主线程的调用栈指的是主线程的调用栈,而其他线程的调用栈则存在于共享内存中.
如下图所示:
线程组
多线程的进程就叫做线程组.线程组中的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应.
进程:task_struct = 1:N
task_struct
{
...
pid_t pid;(线程ID,即PCB的ID)
pid_t tgid;(线程组PID)(getpid()得到的就是tgid)
...
struct task_struct *group_leader;(指向主线程的指针)
...
};
主线程中的pid和tgid相同.tgid是线程组ID,而pid是每个线程的自己的ID.
线程间数据的共享和独立的数据
共享:
1.虚拟地址空间(即使用同一块页表)
2.文件描述符表
3.信号处理方式.
各自的独立的数据:
1.调用栈.
2.线程上下文(一组寄存器).
3.线程ID
4.errno(函数出错时返回的错误码)
5.信号屏蔽字(未决信号集是共用一个,进程收到一个信号,那么所有的线程都会收到)
6.调度优先级
进程的优缺点
优点:
1.创建一个新线程的代价比创建一个新进程要小的多.
2.线程之间共享数据更容易.
3.线程占用的资源要比进程少得多(只需要为线程提供调用栈,线程上下文等)
4.线程之间的切换比进程之间的切换容易
缺点:
1.编码/调试多线程代码变得困难.(由于共享资源可能会存在大量的时序问题)
2.缺乏访问控制,如果一个线程崩溃,那么其余线程都会终止.如果一个进程调用了kill等函数会影响整个进程.
线程的使用场景
1.CPU密集型应用:即将计算分配到多个线程中实现
2.I/O密集型应用
线程控制
POSIX线程库
1.线程的创建
函数原型:
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine),void* arg);
thread:返回线程ID
start_routine:是个函数地址,线程启动执行的函数
arg:线程启动函数的参数
返回值:成功返回1,失败返回0
下面创建了一个线程:
1 #include <stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 void* PthreadEntry(void* arg)
6 {
7 (void)arg;
8 while(1)
9 {
10 printf("pthread\n");
11 sleep(1);
12 }
13 return NULL;
14 }
15 int main()
16 {
17 pthread_t pth;
18 pthread_create(&pth,NULL,PthreadEntry,NULL);
19 while(1)
20 {
21 printf("main pthread\n");
22 sleep(1);
23 }
24 return 0;
25 }
注意:在gcc中,在生成可执行函数时,要加-lpthread,找库.
执行的结果:创建的线程和主线程(main)不确定的进行执行.
总结:多个线程同时存在时,执行是没有先后顺序的.但是如果主线程退出,那么在同一进程中的其余线程也会终止.
2.线程终止
三种方法可终止线程:
1.从线程函数return.(不适合主线程,main函数返回相当于调用exit函数)
2.线程可以调用pthread_exit来终止自己.
3.可以调用pthread_cancel终止同一个进程中的另一个线程.
pthread_exit函数
void pthread_exit(void *value_ptr);
参数:线程入口的返回值.不要指向一个局部变量.
无返回值,结束时无法返回到调用者.
pthread_cancel函数(强制线程终止)
取消一个执行中的线程
int pthread_cancel(pthread_t thread);
参数:线程ID
返回值:成功返回0,失败返回错误码.
下面学习一下这3中方法来终止线程:
1 #include <stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 void *PthreadEntry1(void* arg)
6 {
7 (void)arg;
8 printf("P1\n");
9 return (void*)1;
10 }
11 void *PthreadEntry2(void* arg)
12 {
13 (void)arg;
14 printf("P2\n");
15 pthread_exit((void*)2);
16 return NULL;
17 }
18 void *PthreadEntry3(void* arg)
19 {
20 (void)arg;
21 printf("P3\n");
22 pthread_cancel(pthread_self());
23 return NULL;
24 }
25 int main()
26 {
27 pthread_t p1,p2,p3;
28 pthread_create(&p1,NULL,PthreadEntry1,NULL);
29 pthread_create(&p2,NULL,PthreadEntry2,NULL);
30 pthread_create(&p3,NULL,PthreadEntry3,NULL);
31 void* ret1;
32 pthread_join(p1,&ret1);
33 printf("ret1 = %p\n",ret1);
34 void* ret2;
35 pthread_join(p2,&ret2);
36 printf("ret2 = %p\n",ret2);
37 void* ret3;
38 pthread_join(p3,&ret3);
39 printf("ret3 = %p\n",ret3);
40 return 0;
41 }
执行结果:
3.线程等待
等待的原因:(为了回收资源)
如果退出的线程,没有进行资源释放,仍然在进程的地址空间中.类似于僵尸进程
那么再次创建的新线程就不可以使用退出的进程的资源.
与进程等待的区别:
1.进程只可以父进程等待子进程
2.同一个进程中的线程,都可以等待其他的线程.
3.进程等待有阻塞和非阻塞.而线程等待中,只有一个阻塞等待.
int pthread_join(pthread_t thread,void **value_ptr);
thread:线程ID
value_ptr:指向一个线程的返回值
返回值:成功返回0,失败返回错误码.
调用等待函数后,该线程就会挂起等待,直到thread线程终止.
线程等待可以决定线程结束,调用哪个就会阻塞式的等待哪个线程,直到回收了对应的资源,才会去等待回收其它的线程.
4.线程分离
新创建的线程如果不对它进行线程等待,避免造成系统资源泄露.并且线程等待pthread_join可以决定线程的结束.
如果,当程序员们不关心线程的结束顺序时.只要线程结束时,可以自动的释放资源.那么就可以用将线程分离,在线程结束时就可以自动的释放资源.
线程等待和分离的区别:
手动(线程等待)还是自动(线程分离)回收资源.
线程分离特点:
既可以分离其它线程也可以分离自身.
充分利用多核CPU:
1 #include <stdio.h>
2 #include<unistd.h>
3 #include<pthread.h>
4
5 void *Pthread(void* arg)
6 {
7 (void)arg;
8 while(1)
9 {}
10 return NULL;
11 }
12 int main()
13 {
14 pthread_t pth[4];
15 int i = 0;
16 for(i = 0;i < 4;++i)
17 {
18 pthread_create(&pth[i],NULL,Pthread,NULL);
19 }
20 for(i = 0;i < 4;++i)
21 {
22 pthread_join(pth[i],NULL);
23 }
24 return 0;
25 }
通过top指令查看的结果: