一、Linux线程概念
1、线程的概念
在一个程序里的一个执行路线就叫做线程(thread),所以线程是一个执行分支,其执行粒度比进程更细,调度成本更低。
我们知道一个进程包含进程控制块(task_struct
)、进程地址空间(mm_struct
)以及页表等许多内容。
如果我们在创建一个“新进程”时,只创建task_struct
,并要求创建出来的新task_struct
和已经存在的老task_struct
共享进程地址空间和页表。这时我们就会发现我们新创建的task_struct
好像也是一个"进程",也是一个独立的执行流,其实这就是创建一个线程。
所以从内核观点来看:线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体
问:为什么说:线程其执行粒度比进程更细?
由于一个进程中的所有的线程看到的是同一份代码,所以我们可以让不同的线程执行不同部分的代码,这样每个线程就能更精确的执行完成任务。
问:为什么说:线程的调度成本比进程更低?
由于局部性原理CPU内部的多级缓存(Cache)存放的是当前进程的代码和数据的部分内容,如果一个进程要进行切换,那么多级缓存里面的内容就要进行重新缓存(这里的成本很高),如果只是线程切换那么就不必重新缓存,于是线程的调度成本比进程更低。
由于线程的存在,操作系统要进行管理线程,比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源以及管理存储的线程的属性,这所有的这一套管理机制相比较进程都需要另起炉灶,搭建一套与进程平行的线程管理机制。
因此,如果要支持真线程一定会提高一款操作系统的复杂程度。在Linux
看来,描述线程的控制块(TCB)和描述进程的控制块(PCB)是类似的,而且其管理办法也是类似的,因此Linux
并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中的所有执行流都叫做轻量级进程(LWP),由于这样的设计也让Linux
更好维护,效率更高。
但也有支持真的线程的操作系统,比如Windows
操作系统,因此Windows
操作系统系统的实现逻辑一定比Linux
操作系统的实现逻辑要复杂得多。
2、结论
- 在一个程序里的一个执行路线就叫做线程(thread),更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行。
Linux
下并不存在真正的多线程!而是用进程模拟的!- 在
Linux
系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。 - 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
3、线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多。 - 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
概念说明:
- 计算密集型:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找等。
- IO密集型:执行流的大部分任务,主要以IO为主。比如刷磁盘、访问数据库、访问网络等。
4、线程的缺点
- 性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
5、线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
6、线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
二、Linux线程控制
1、vfork函数
在Linux
没有真正意义的线程,那么也就绝对没有真正意义上的线程相关的系统调用!但是Linux可以提供创建轻量级进程(LWP)的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork
函数。
vfork
函数的功能就是创建子进程,但是父子共享空间,vfork
的函数原型如下:
vfork
函数的返回值与fork
函数的返回值相同:给父进程返回子进程的PID,给子进程返回0。
vfork是这样的工作的:
(1)保证子进程先执行。
(2)当子进程调用exit()
或exec()
后,父进程往下执行。
例如在下面的代码中,父进程使用vfork函数创建子进程,子进程将变量val由0改为了100,父进程休眠1秒后再读取到变量val的值。
#include <iostream> #include <cstdlib> #include <sys/types.h> #include <unistd.h> int main() { int val = 0; pid_t pid = vfork(); if(pid == 0) { val = 100; std::cout << "我是子进程,我的pid是:" << getpid() << ",ppid是:" << getppid() << std::endl; // 注意这里必须使用exit(),不能使用return, return 会弹栈导致后续父进程无法执行 exit(0); } sleep(1); std::cout << "我是父进程,我的pid是:" << getpid() << ",ppid是:" << getppid() << std::endl; std::cout << "val:" << val << std::endl; return 0; }
2、POSIX线程库
Linux
中,在内核角度没有真正意义上线程相关的接口,但是在用户角度,当用户想创建一个线程时更期望使用thread_create
这样类似的接口,而不是vfork
函数,因此系统为用户层提供了原生线程库pthread
。
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。
库的简单介绍
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以
pthread_
打头的 - 要使用这些函数库,要通过引入头文
<pthread.h>
- 链接这些线程函数库时要使用编译器命令的
-l pthread
选项 - 原生指的是大部分
Linux
系统都会默认带上该线程库。
phread函数的一些注意点
- 传统的一些函数或者系统调用是,成功返回0,失败返回-1,并且对全局变量
errno
赋值以指示错误。 pthread
系列函数出错时不会设置全局变量errno
(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。pthread
系列同样也提供了线程内的errno
变量,以支持其他使用errno
的代码。对于pthread
函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno
变量的开销更小。
3、线程创建
线程创建的函数是:
int pthread_create( pthread_t* thread, const pthread_attr_t* attr, void* (*start routine)(void* ), void* arg );
参数:
thread
:pthread_t
是一种类型表示线程号,pthread_ create
函数会产生一个线程ID,存放在第一个参数指向的地址中。attr
:设置线程的属性,attr
为nullptr
表示使用默认属性start_routine
:是个函数地址,线程启动后要执行的函数arg
:传给线程启动函数的参数
返回值:
- 成功返回
0
,失败返回错误码。
线程库也提供了pthread_ self
函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
下面的代码创建两个线程并分别打印自己的线程号和传递的数据
#include <iostream> #include <cstdio> #include <unistd.h> #include <pthread.h> void* thread_1(void* args) { const char* s = (char*)args; while (1) { std::cout << "我是线程:" << pthread_self() << ", 我的任务是:" << s << std::endl; sleep(1); } } void* thread_2(void* args) { const char* s = (char*)args; while (1) { std::cout << "我是线程:" << pthread_self() << ", 我的任务是:" << s << std::endl; sleep(1); } } int main() { pthread_t t1, t2; const char* s1 = "等待任务"; const char* s2 = "计算任务"; // 创建线程 pthread_create(&t1, nullptr, thread_1, (void*) s1); sleep(1); pthread_create(&t2, nullptr, thread_2, (void*) s2); while (1) { std::cout << "我是主线程,我的线程号" << pthread_self() << std::endl; sleep(1); } return 0; }
可以看出那个线程先被调度是没有办法确定的,这完全由调度器决定!
4、线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数
return
。这种方法对主线程不适用,从main
函数return
相当于调用exit
。 - 线程可以调用
pthread_ exit
终止自己。 - 一个线程可以调用
pthread_ cancel
终止同一进程中的另一个线程。
线程终止函数是:
void pthread_exit(void *value_ptr);
参数:
value_ptr
:通过value_ptr
在线程终止时能向其他线程输出数据,value_ptr
不能指向一个局部变量。
pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
返回值:
- 无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
线程取消函数:
int pthread_cancel(pthread_t thread);
参数:
thread
:线程ID
返回值:
- 成功返回0;失败返回错误码
下面的代码在上面代码的基础上增加了线程的取消和终止。
#include <iostream> #include <cstdio> #include <unistd.h> #include <pthread.h> void* thread_1(void* args) { const char* s = (char*)args; int cnt = 2; while (cnt--) { std::cout << "我是线程:" << pthread_self() << ", 我的任务是:" << s << std::endl; sleep(1); } // 线程退出 pthread_exit((void*)0); } void* thread_2(void* args) { const char* s = (char*)args; while (1) { std::cout << "我是线程:" << pthread_self() << ", 我的任务是:" << s << std::endl; sleep(1); } } int main() { pthread_t t1, t2; const char* s1 = "等待任务"; const char* s2 = "计算任务"; pthread_create(&t1, nullptr, thread_1, (void*) s1); sleep(1); pthread_create(&t2, nullptr, thread_2, (void*) s2); int cnt = 4; while (cnt--) { std::cout << "我是主线程,我的线程号" << pthread_self() << std::endl; sleep(1); } // 取消线程 pthread_cancel(t2); std::cout << "线程" << t2 << "已经取消" << std::endl; return 0; }
补充:
- 线程可以自己取消自己,取消成功的线程的退出码一般是
-1
。 - 新线程可以取消主线程,线程取消时主线程和各个新线程之间的地位是对等的,取消主线程,其他线程也是能够跑完的,只不过主线程不再执行后续代码了。
pthread_cancel()
函数的功能仅仅是向目标线程发送Cancel
信号,至于目标线程是否处理该信号以及何时结束执行,由目标线程决定。
5、线程等待
与进程一样,线程也是需要等待的,这时因为:
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
线程等待的函数:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread
:线程IDvalue_ptr
:它指向一个指针,后者指向线程的返回值。
返回值:
- 成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread
的线程终止。thread
线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的,总结如下:
- 如果
thread
线程通过return
返回,value_ ptr
所指向的单元里存放的是thread
线程函数的返回值。 - 如果
thread
线程是自己调用pthread_exit
终止的,value_ptr
所指向的单元存放的是传给pthread_exit
的参数。 - 如果
thread
线程被别的线程调用pthread_ cancel
异常终掉,value_ ptr
所指向的单元里存放的是常数PTHREAD_ CANCELED
,其本质是-1
。
#define PTHREAD_CANCELED ((void *) -1)
- 如果对
thread
线程的终止状态不感兴趣,可以传nullptr
给value_ ptr
参数。
下面的代码利用了多线程进行计算0-x的求和。
#include <iostream> #include <cstdio> #include <unistd.h> #include <pthread.h> #define NUM 5 class ThreadData { public: ThreadData(int bondary) : _upper_bondary(bondary) ,_sum(0) {} public: // 计算的上边界 int _upper_bondary; // 计算的结果 int _sum; }; // 执行计算的线程 void* thread_1(void* args) { ThreadData* ptd = static_cast<ThreadData*> (args); std::cout << "正在计算..." << std::endl; // 执行累加 1 到 _upper_bondary for (int i = 0; i <= ptd->_upper_bondary; i++) { ptd->_sum += i; } pthread_exit(ptd); } int main() { // 创建多个线程进行计算 pthread_t tname[NUM]; for (int i = 0; i < NUM; i++) { ThreadData* ptd = new ThreadData(100 + i * 5); pthread_create(tname + i, nullptr, thread_1, ptd); sleep(1); } // 进行结果归纳 void* ret = nullptr; for (int i = 0; i < NUM; i++) { // 线程等待 int error = pthread_join(tname[i], &ret); ThreadData* ptd = static_cast<ThreadData*> (ret); if (error == 0) { std::cout << "等待成功, 计算结果是:" << ptd->_sum << ", (他要计算的结果是[0-" << 100 + 5 * i << "])" << std::endl; } else { std::cerr << "等待失败" << std::endl; } // 注意释放内存 delete ptd; } std::cout << "等待完毕!" << std::endl; return 0; }