Linux从青铜到王者第十三篇:Linux多线程四万字详解
Posted 森明帮大于黑虎帮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux从青铜到王者第十三篇:Linux多线程四万字详解相关的知识,希望对你有一定的参考价值。
系列文章目录
文章目录
前言
一、Linux线程概念
1.什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
2.线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
3.线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
4.线程的异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
1 #include<iostream>
2 #include<pthread.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 using namespace std;
6
7 void* thread_run(void* arg)
8 {
9 pthread_detach(pthread_self());
10 while(1)
11 {
12 cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
13 sleep(1);
14 break;
15 }
16 int a=10;
17 a=a/0;
18 return (void*)10;
19 }
20 int main()
21 {
22 pthread_t tid;
23 int ret=0;
24 ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
25 if(ret!=0)
26 {
27 return -1;
28 }
29
30 sleep(10);
31 pthread_cancel(tid);
32 cout<<"new thread "<<tid<<" be cancled!"<<endl;
33 void* tmp=NULL;
34 pthread_join(tid,&tmp);
35 cout<<"thread qiut code:"<<(long long )ret<<endl;
36 return 100;
37 }
5.线程的用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
二、进程和线程的对比
1.进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据
- 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境。
2.多进程的应用场景有哪些?
三、线程控制
1.POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
- 要使用这些函数库,要通过引入头文<pthread.h>。
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
2.创建线程
- int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
- pthread_t:线程标识符,本质上是线程在共享区独有空间的首地址。
-pthread_t:是一个出参,该值由pthread_creat函数赋值的。
-thread:创建线程的属性,一般情况都指定为NULL,采用默认属性。- pthread_attr_t:函数指针,接收一个返回值为void*,参数为void*的函数地址,就是线程入口函数。
- void *(*start_routine) (void *):给线程入口函数传递的参数;由于参数类型是void*,返回值类型为void*,所以给了程序无限的传递参数的方式(char*,int*,结构体指针,this)
- 返回值:
失败:< 0
在主线程中创建一个工作线程,主线程和副线程都不退出。
mythead.cpp
1 #include<iostream>
2 #include<pthread.h>
3 using namespace std;
4 #include<unistd.h>
5
6 void* thread_run(void* arg)
7 {
8 while(1)
9 {
10 cout<<"i am "<<(char*)arg<<endl;
11 sleep(1);
12 }
13 }
14 int main()
15 {
16 pthread_t tid;
17 int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
18 if(ret!=0)
19 {
20 return -1;
21 }
22 while(1)
23 {
24 cout<<"i am main thread"<<endl;
25 sleep(2);
26 }
27 return 0;
28 }
makefile
1 mythread:mythread.cpp
2 g++ $^ -o $@ -lpthread
3 .PHONY:clean
4 clean:
5 rm -f mythread
在主线程中创建一个副线程,让主线程退出,副线程不退出。
1 #include<iostream>
2 #include<pthread.h>
3 using namespace std;
4 #include<unistd.h>
5
6 void* thread_run(void* arg)
7 {
8 while(1)
9 {
10 cout<<"i am "<<(char*)arg<<endl;
11 sleep(1);
12 }
13 }
14 int main()
15 {
16 pthread_t tid;
17 int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
18 if(ret!=0)
19 {
20 return -1;
21 }
22 return 0;
23 }
下图可以看到主线程退出来,进程也退出。
- 传参问题验证:
- 假设要往创建的工作线程中传入一个参数1,首先要将参数强转为(void*)类型,然后将参数的地址传入,而在工作线程中使用是只需将(void*)转换为(int*)即可。
1 #include<iostream>
2 #include<unistd.h>
3 #include<pthread.h>
4 using namespace std;
5
6 void* MyThreadStrat(void* arg)
7 {
8 int* i=(int*)arg;
9 while(1)
10 {
11 cout<<"MyThreadStrat:"<<*i<<endl;
12 sleep(1);
13 }
14 return NULL;
15 }
16 int main()
17 {
18 pthread_t tid;
19 int i=1;
20 int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
21 if(ret!=0)
22 {
23 cout<<"线程创建失败!"<<endl;
24 return 0;
25 }
26 while(1)
27 {
28 sleep(1);
29 cout<<"i am main thread"<<endl;
30 }
31 return 0;
32 }
虽然参数可以正常传入,但实际是存在一定的错误的,因为局部变量 i 传入的时候生命周期未结束,而在传递给工作线程的时候生命周期结束了,那么这块局部变量开辟的区域就会自动释放,而此时工作线程还在访问这块地址,就会出现非法访问。
代码改成循环:
1 #include<iostream>
2 #include<unistd.h>
3 #include<pthread.h>
4 using namespace std;
5
6 void* MyThreadStrat(void* arg)
7 {
8 int* i=(int*)arg;
9 while(1)
10 {
11 cout<<"MyThreadStrat:"<<*i<<endl;
12 sleep(1);
13 }
14 return NULL;
15 }
16 int main()
17 {
18 pthread_t tid;
19 int i=0;
20 for( i=0;i<4;i++)
21 {
22 int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
23 if(ret!=0)
24 {
25 cout<<"线程创建失败!"<<endl;
26 return 0;
27 }
28 }
29 while(1)
30 {
31 sleep(1);
32 cout<<"i am main thread"<<endl;
33 }
34 return 0;
35 }
因为for循环4次最终开辟4个工作线程,开辟线程传递进去的是 i 的地址,而 i 中的值从0加到4,而 i 到5退出,此时 i 已经被加为4,最终 i 的地址中存的值为 4,使用最终会一直输出4。
问题的解决---->动态内存开辟:
传递this指针:
class MyThread
{
public:
MyThread()
{
}
~MyThread()
{
}
int Start()
{
int ret = pthread_create(&tid_, NULL, MyThreadStart, (void*)this);
if(ret < 0)
{
return -1;
}
return 0;
}
static void* MyThreadStart(void* arg)
{
MyThread* mt = (MyThread*)arg;
printf("%p\\n", mt->tid_);
}
private:
pthread_t tid_;
};
int main()
{
return 0;
}
传递结构体指针:
1 #include<iostream>
2 #include<unistd.h>
3 #include<pthread.h>
4 using namespace std;
5
6 struct ThreadId
7 {
8 int thread_id;
9 };
10 void* MyThreadStrat(void* arg)
11 {
12 struct ThreadId* tid=(struct ThreadId*)arg;
13 while(1)
14 {
15 cout<<"MyThreadStrat:"<<tid->thread_id<<endl;
16 sleep(1);
17 }
18 delete tid;
19 }
20 int main()
21 {
22 pthread_t tid;
23 int i=0;
24 for( i=0;i<4;i++)
25 {
26 struct ThreadId* id=new ThreadId();
27 id->thread_id=i;
28 int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)id);
29 if(ret!=0)
30 {
31 cout<<"线程创建失败!"<<endl;
32 return 0;
33 }
34 }
35 while(1)
36 {
37 sleep(1);
38 cout<<"i am main thread"<<endl;
39 }
40 return 0;
41 }
3.进程ID和线程ID
- 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
- 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
- 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结
以上是关于Linux从青铜到王者第十三篇:Linux多线程四万字详解的主要内容,如果未能解决你的问题,请参考以下文章
Lua从青铜到王者基础篇第十三篇:Lua 调试(Debug)
Love2d从青铜到王者第十三篇:Love2d之游戏:射击敌人(Game: Shoot the enemy)