新线程确实可以接收到主线程发送的信息,但是新线程的id却非常怪,这里可以用16进制的方式打印出来看看。
最后发现这是一个地址,那么是什么地址呢?
这个后面再说。
这里补充一点,线程一旦创建,几乎所有的资源都是被所有线程共享的。
#include <iostream> #include <pthread.h> #include <cassert> #include <unistd.h> using namespace std; int i = 0; void *pthread_routine(void* args) { const char* p = (const char*)args; while(true) { cout << "我是新线程"<< "i:"<< i++ << "&i:" << &i << endl; sleep(1); } } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void* assert(n == 0); (void)n; while(true) { char buffer[64]; snprintf(buffer, sizeof(buffer),"0x%x", tid); cout << "我是主线程" << "i:"<< i << "&i:" << &i << endl; sleep(1); } return 0; }
虽然线程共享了大部分资源,但是线程也一定会有私有属性,都有什么呢?
1.线程只要会被CPU调度,那么PCB属性就是私有的。
2.上下文一定是私有的,不然怎么独立调度呢?
3.拥有独立的栈结构。(用来保存自己的数据)
2和3是证明线程动态运行的证据。
与进程之间切换相比,线程需要操作系统左的工作会少很多,为什么呢?
1.线程不需要切换页表和虚拟地址空间
2.CPU中有一个叫做cache,其实就是高速缓存。这个功能就是保存热点数据(就是保存这条代码附近的代码,因为很有可能会访问到附近的代码)
也就是说如果某个进程先让部分代码放入eache中,然后CPU去这里找,如果未命中才会重新去内存中找。
也就是说如果是进程之间切换,不同的进程数据是不共享的,降低了效率。
但是线程的数据是共享的,eache可以不用切换。
总结:
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
一切进程至少都有一个执行线程。
线程在进程内部运行,本质是在进程地址空间内运行。
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
线程的优缺点
优点:
创建一个新线程的代价要比创建一个新进程小得多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用(CPU,加密,解密,算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。(并不是线程越多越好,要合适,最好要和CPU的核数相同)
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
Linux进程VS线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
线程的异常
一个线程出异常,会影响到另外线程。
#include <iostream> #include <pthread.h> #include <cassert> #include <unistd.h> using namespace std; void *pthread_routine(void* args) { while(true) { cout << "我是新线程" << endl; sleep(1); int* p = nullptr; *p = 0; } } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, pthread_routine, (void *)"111111");//参数记得强制转换成void* assert(n == 0); (void)n; while(true) { cout << "我是主线程" << endl; sleep(1); } return 0; }
一旦出异常的时候,会给这个进程发送信号,发送信号也是发送给所有线程,然后就会终止所有线程。
创建线程两个的接口
之前说过,程序员只需要线程,但是Linux又不直接提供创建线程的接口,只提供第三方库(软件层次)创建轻量级进程的接口,下面来介绍这些接口。
创建轻量级进程或者进程的底层接口:(区别就是创建的时候是否共享地址空间)
第一个参数是新执行流要执行的代码,第二个参数是栈结构。
这个接口是和fork差不多,只不过是共享了地址空间。
但是两个接口不是很常用
线程的控制
线程的创建
pthread_create
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(start_routine)(void), void *arg);
参数
thread:返回线程ID。
attr:设置线程的属性,attr为NULL表示使用默认属性。
start_routine:是个函数地址,线程启动后要执行的函数。
arg:传给线程启动函数的参数。
返回值:成功返回0;失败返回错误码。
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。
首先来创建一组线程来看看:
#include <iostream> #include <pthread.h> #include <cassert> #include <string> #include <vector> #include <unistd.h> using namespace std; void *pthread_routine(void* args) { string name = static_cast<const char*>(args); while(true) { cout << "new " << name << endl; sleep(1); } } int main() { vector<pthread_t> tids; #define NUM 10 for(int i = 0; i < NUM; i++) { pthread_t tid; char buffer[64]; snprintf(buffer,sizeof(buffer),"%s:%d","thread",i); int n = pthread_create(&tid, nullptr, pthread_routine, (void *)buffer); sleep(1); } while(true) { cout << "main thread" << endl; sleep(1); } return 0; }
如果将主线程的创建线程过程中的sleep注释掉会发生什么呢?
有些编号的线程完全没有出现过,这是为什么呢?
因为创建线程之后哪个线程先运行是不确定的,并且:
这个函数的最后一个参数传过去的是缓冲区的起始地址。
有时候某些进程先运行:
那么这种情况如何避免呢?
#include <iostream> #include <pthread.h> #include <cassert> #include <string> #include <vector> #include <unistd.h> using namespace std; struct ThreadData { pthread_t tid; char buffer[64]; }; void *pthread_routine(void* args)//当前函数是被重入状态,如果忽略打印操作就是可重入函数 { ThreadData* p = static_cast<ThreadData*>(args); while(true) { cout << "new " << p->buffer << endl; sleep(1); } delete p; } int main() { #define NUM 10 for(int i = 0; i < NUM; i++) { ThreadData *p = new ThreadData;//这里每个创建的起始地址都不相同 char buffer[64]; snprintf(p->buffer,sizeof(p->buffer),"%s:%d","thread",i); pthread_create(&p->tid, nullptr, pthread_routine, p); } while(true) { cout << "main thread" << endl; sleep(1); } return 0; }
那么pthread_routine为什么能可重入呢?
因为每一个线程都有自己独立的栈结构。