营销型网站建设需要懂什么,做网站哪里最好,mir设计公司官网,便宜网站建设模板网站POSIX线程库
引言
前面我们提到了Linux中并无真正意义上的线程 从OS角度来看#xff0c;这意味着它并不会提供直接创建线程的系统调用#xff0c;它最多给我们提供创建轻量级进程LWP的接口 但是从用户的角度来看#xff0c;用户只认识线程啊#xff01; 因此#xff0c;…POSIX线程库
引言
前面我们提到了Linux中并无真正意义上的线程 从OS角度来看这意味着它并不会提供直接创建线程的系统调用它最多给我们提供创建轻量级进程LWP的接口 但是从用户的角度来看用户只认识线程啊 因此操作系统OS与用户两者之间必定存在一个桥梁——库 这个线程库对下能够将Linux提供的LWP进程接口进行封装对上能够给用户进行线程控制的接口 这个库我们就称作pthread库在里面的绝大多数函数的名字都是以“pthread_”打头的 在任何linux系统下不管版本的老旧都会默认自带是一个原生线程库等下我们也会进行验证
前提
但是pthread线程库并非随意就能使用还需要我们编写代码时进行一些附加操作 1.要使用这些函数库要引入头文件pthread.h 2.链接这些线程函数库时要使用编译器命令的“-lpthread”选项 1 mythreadTest:threadTest.cc2 g -o $ $^ -stdc11 -lpthread 3 .PHONY:clean4 clean:5 rm -f mythreadTest
线程创建
就像每个文件都有着其对应的inode编号每个线程也有着自己的编号它的类型时pthread_t类型 假如在编译器中一直跳转寻找它最开始的定义 可以发现它实际上就是一个unsigned long类型 不过具体这个编号有什么用我们先按下不表 我们先介绍创建线程提供的pthread_create函数 man手册查该函数会给出相应的函数介绍 可以看到该函数位于3号手册中所以也符合我们前面的讲解即该函数不是系统调用的函数而是封装了linux系统的轻量级进程接口的函数 功能是创建一个新线程(create a new thread) 总共有4个参数 第一个参数thread 是线程id的地址(返回线程ID) 第二个参数attr, 设置线程的属性attr为NULL表示使用默认属性通常使用的时候都给nullptr使用默认属性 第三个参数start_routine 是个函数地址线程启动后要执行的函数,参数是void*,返回参数也是void*是一个函数指针 第四个参数arg 是等下传给start_routine的参数(传给线程启动函数的参数) 有了上面的基础后我们就可以先简单创建一个我们的线程 主线程输出对应的线程id 另外一个线程输出自己正在允许 1 #include iostream2 #include unistd.h3 4 using namespace std;5 void* thread_run(void* args)6 {7 while(true)8 {9 cout new thread is running endl;10 sleep(1); 11 }12 }13 14 int main()15 {16 pthread_t t;17 pthread_create(t,nullptr,thread_run,nullptr);18 19 while(true)20 {21 cout main thread is running,thread id : t endl;22 sleep(1);23 }24 return 0;25 }
可以看到结果符合我们的预期 往命令行窗口输入lld 对应文件名即可看到该文件链接了什么库 可以看到其中有一个pthread库它对应的路径是/lib64/libpthread,也就和我们之前所说的任何Linux系统默认自带相应的pthread库说法完美符合 但是我们现在只是创建了一个线程而已所以我们的代码肯定还是要改进创建更多的线程的 我们采取数组的方式我们知道数组名实际上就是首元素的地址加上对应的i实际对应的刚好就是数组里面每个元素的地址而不用再取地址 并且我们可以开始研究第四个参数arg它是主线程往新线程里面传的参数 那实际上能不能传过去呢我们对传进来的参数args进行强制类型转换然后打印相应的内容假如能够打印相应的内容则说明args这个参数的确能够是主线程往新线程里面传的参数 1 #include iostream2 #include unistd.h3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7 char* name (char*)args;8 9 while(true)10 {11 cout new thread: name is running endl;12 sleep(1);13 }14 15 return nullptr; 16 }17 18 int main()19 {20 pthread_t tid[NUM];21 for (int i 0;i NUM;i)22 {23 char tname[64];24 snprintf(tname,sizeof(tname),thread-%d,i 1);25 pthread_create(tid i,nullptr,thread_run,tname);26 }27 28 29 while(true)30 {31 cout main thread is running endl;32 sleep(1);33 }34 return 0;35 }
但是打印出来的结果却不符合我们的预期 第一我们预想的是每个线程的编号都应该不同即每个线程的名字都不一样毕竟我们循环往tname这个数组里面放内容的时候用的是不同的i 第二,有部分线程输出代码紧挨在一起并且主线程并不是最先运行的反而是新线程先运行 对于第二个问题我们其实可以解释在进程的一章中我们就已经提到过哪个进程先被调度其实是不确定的同样的线程也是我们调用轻量级进程接口创建出来的肯定也是符合这个规律所以谁先被调度完全取决于调度器决定先创建的线程不一定被调度 对于第一个问题就有点难理解 实际上是由于线程共享的是同一份资源即便这只是一个临时变量 因此tname里面存的地址在不同线程看来都是相同的 所以往里面同时写数据就会覆盖原有tname空间的旧内容 最后剩下的仅仅是最后调度的线程的名字 那我们要怎么修改呢 一种简单的方式就是直接new相应的空间 (不过要注意此时使用snprintf函数的时候就不能再直接sizeof这样计算的就单纯只会是指针的大小所以这里直接指定64字节毕竟整个空间也就64字节) 对于每个线程来说都会new出新的自己的空间这样放的数据就不会再被覆盖 相当于每个线程都有了自己的房子从此井水不犯河水 1 #include iostream2 #include unistd.h3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 {7 char* name (char*)args;8 9 while(true)10 {11 cout new thread: name is running endl;12 sleep(1);13 }14 delete name; 15 return nullptr;16 }17 18 int main()19 {20 pthread_t tid[NUM];21 for (int i 0;i NUM;i)22 {23 char* tp new char[64];24 snprintf(tp,64,thread-%d,i 1);25 pthread_create(tid i,nullptr,thread_run,tp);26 }27 28 29 while(true)30 {31 cout main thread is running endl;32 sleep(1);33 }34 return 0;35 }
经过修改后的运行效果就符合我们的预期了
线程终止
前面我们提到了在linux系统下是没有对应具体线程的实现而是采用复用的方式 所以进程有的特性线程往往也会有
主线程提前退出
假如主线程现在提前退出了说不再和其它新线程一起玩会发生什么情况呢 将上面主线程的代码修改一下把循环去掉 此时再编译运行我们的代码会得到下面的结果 可以看到一旦主线程退出了其它的所有新线程就会全部强制退出 为什么会出现这种情况呢 就是因为线程是进程的一个执行分支线程异常了发送信号是给进程发信号进程挂了所有依附于它的线程全部都走不了覆巢之下安有完卵指的就是这个道理 同样的假如其中一个线程调用了exit函数那请问最后的结果会是怎么样呢 我们同样可以修改相应的代码在循环中加入相应的exit函数 可以看到只有几个线程成功输出了自己的编号程序就自动停止了 所以实际的情况就是有几个线程成功被创建但是其中有一个线程执行exit函数然后全部线程都挂掉了 只要有任何一个线程调用exit函数整个进程中所有的线程都会全部退出 关键不是主线程还是新线程的问题而是大家都是一体的同生共死
阻塞等待
正是由于线程和进程是有很多相似之处的进程有父进程阻塞等待回收子进程的操作 线程也会有相应的概念 主线程需要等待子线程然后进行相应的回收否则子线程就会陷入僵尸状态 在pthread库里面就已经提供了相应主线程等待的库函数pthread_join 调用该函数主线程就会阻塞并回收相应退出的新线程(join with a terminated thread) 它总共有两个参数‘ 第一个参数thread就是我们之前提到过的线程id 第二个参数retval是一个二级指针void**为什么是二级指针呢 因为它是一个输出型参数早在C语言函数中我们就已经学过由于C语言中没有引用的概念因此在函数内部进行赋值其实改变的都是形参并不会改变实参 想要改变实参就需要传相应的指针 想传int出来就要int* 想传int出来就要int**作为参数 同理假如我们返回的参数此时是一个void类型的那用void**接收也就非常合理了 它的返回值和前面提到过的pthread_create函数相同 成功的话就返回0否则返回一个错误的数字 1 #include iostream2 #include unistd.h3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7 char* name (char*)args;8 9 while(true)10 {11 cout new thread: name is running endl;12 sleep(1);13 } 14 delete name; 15 return nullptr; 16 } 17 18 int main() 19 { 20 pthread_t tid[NUM]; 21 for (int i 0;i NUM;i) 22 { 23 char* tp new char[64]; 24 snprintf(tp,64,thread-%d,i 1); 25 pthread_create(tid i,nullptr,thread_run,tp); 26 } 27 28 for (int i 0;i NUM;i)29 {30 pthread_join(tid[i],nullptr);31 }32 33 return 0;34 }终止方式
既然我们知道主线程需要阻塞等待子线程退出并回收相应的子线程 那了解子线程有多少种退出方式就非常有必要 子线程总共有三种退出方式 第一种方式线程函数执行完毕此时直接返回nullptr线程就会退出 1 #include iostream2 #include unistd.h3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7 char* name (char*)args;8 9 while(true)10 {11 cout new thread: name is running endl;12 sleep(1);13 break;14 }15 delete name;16 return nullptr;17 }18 int main()19 { 20 pthread_t tid[NUM]; 21 for (int i 0;i NUM;i) 22 { 23 char* tp new char[64]; 24 snprintf(tp,64,thread-%d,i 1); 25 pthread_create(tid i,nullptr,thread_run,tp); 26 } 27 28 for (int i 0;i NUM;i) 29 { 30 int n pthread_join(tid[i],nullptr); 31 //errno变量只有一个而线程有多个作同时修改可能会互相影响 32 if(n! 0) cerr pthread_join error endl; 33 } 34 35 return 0; 第二种方式pthread库里面提供了相应的线程退出函数pthread_exit 它的参数retval为一个输出型参数 没错和我们之前提到的pthread_join的参数名字是相同的也就意味着两者肯定有所关联 通过返回retval我们对应的主线程就可以接收到对应的错误信息 那为什么我们不通过设置全局变量errno来输出对应的错误信息呢 因为不同线程对于这个全局变量是共享的全部线程都同时使用一个全局变量就可能会出现覆盖等等问题导致出错了也可能不知道 因此pthreads函数出错时不会设置全局变量errno而大部分其他POSIX函数会这样做 而是将错误码通过返回值返回 还有一个好处是对于pthreads函数的错误通过返回值判定要比读取线程内的errno变量的开销更小 1 #include iostream2 #include unistd.h3 #define NUM 104 using namespace std;5 void* thread_run(void* args)6 { 7 char* name (char*)args;8 9 while(true)10 {11 cout new thread: name is running endl;12 break;13 }14 delete name;15 pthread_exit(nullptr); 16 }17 18 19 int main()20 {21 pthread_t tid[NUM];22 for (int i 0;i NUM;i)23 {24 char* tp new char[64];25 snprintf(tp,64,thread-%d,i 1);26 pthread_create(tid i,nullptr,thread_run,tp);27 }28 29 for (int i 0;i NUM;i)30 {31 int n pthread_join(tid[i],nullptr);32 //errno变量只有一个而线程有多个作同时修改可能会互相影响33 if(n! 0) cerr pthread_join error endl;34 }35 cout all thread quit endl;36 return 0;37 } 1 #include iostream2 #include unistd.h3 #include pthread.h4 #include string5 #include ctime6 #define NUM 107 using namespace std;8 9 10 class ThreadData11 {12 public:13 ThreadData(const string name,int id,time_t createTime):_name(name),_id(id),_createTime((uint64_t)createTime)14 {}15 ~ThreadData()16 {}17 public:18 string _name;19 int _id;20 uint64_t _createTime;21 };22 void* thread_run(void* args)23 {24 ThreadData* tp static_castThreadData*(args);25 26 while(true)27 {28 cout thread is running,name: tp-_name create time: tp-_createTime index: tp-_id endl; 29 break;30 }31 delete tp;32 pthread_exit((void*)2);33 }34 35 36 int main()37 {38 pthread_t tid[NUM];39 for (int i 0;i NUM;i)40 { 41 char tname[64];42 snprintf(tname,sizeof(tname),thread-%d,i 1);43 ThreadData* tp new ThreadData(tname,i 1,time(nullptr));44 pthread_create(tid i,nullptr,thread_run,tp);45 }46 47 void* ret nullptr;48 for (int i 0;i NUM;i)49 {50 int n pthread_join(tid[i],ret);51 //errno变量只有一个而线程有多个作同时修改可能会互相影响52 if(n! 0) cerr pthread_join error endl;53 54 cout thread quit: (uint64_t)ret endl;55 }56 cout all thread quit endl;57 return 0;58 }
最后一种方式是一个线程可以调用pthread_ cancel函数终止同一进程中的另一个线程 整个函数只需要一个参数也即是我们的线程id 它的功能就是取消一个执行中的线程 成功返回0反之则返回错误码
类型转换
在C或者C中我们都知道一个类型的值赋值给不匹配的类型变量就会发生报错 但是我们仔细思考一下在计算机的眼里不同数据有区别吗 都只是01的集合罢了 所以报错是编译器检测发现你类型不匹配然后报错显示无法编译你的代码 所谓的类型转换就是让我们骗过编译器让数据能够赋到我们想要的变量中 比如说下面的代码1还是那个1但是是int类型 你需要将它类型转换告诉编译器这个1其实是一个地址这样才能成功赋值 void* ret (void*)1; 进一步思考的话类型转换也告诉了OS这究竟是什么类型变量 这非常关键决定我们将它存到哪里它的偏移地址是什么等等这样我们以后才能成功访问到这个数据
void*
所以为什么无论是我们pthread_create函数还是我们的pthread_exit函数它们的参数中设计的都是void* 为的是什么 为的就是我们让我们传入参数和返回参数的可塑性更强它并非局限我们只能传一个字符串作为线程函数传入参数或者只能返回对应的错误码 我们是可以传intdouble*等等所有的指针甚至我们是可以传对象指针进去* 只需要void*接收然后再类型转换为我们想要的类型就可以让OS找到对应的资源 下面这段代码就实现了传一个对象进去线程函数里面并且通过返回这个对象的指针将里面处理好的结果带出来 整段代码实现的功能 就是让不同的线程分别实现从1到对应top数字的求和 原本的串行执行转变为现在的并发执行 1 #include iostream2 #include unistd.h3 #include pthread.h4 #include string5 #include ctime6 #define NUM 107 using namespace std;8 enum{ ERROR 0,OK };9 10 class ThreadData11 {12 public:13 ThreadData(const string name,int id,time_t createTime,int top):_name(name),_id(id),_createTime((uint64_t)createTime),_status(OK),_top(top),_result(0)14 {}15 ~ThreadData()16 {}17 public:18 //传入的参数19 string _name;20 int _id;21 uint64_t _createTime;22 23 //返回的参数24 int _status; //该线程的参数25 int _top;26 int _result; //结果是什么27 //char arr[n];28 };29 void* thread_run(void* args)30 { 31 ThreadData* tp static_castThreadData*(args);32 33 for (int i 1;i tp-_top;i)34 {35 tp-_result i;36 }37 38 cout tp-_name: tp-_name endl;39 return tp;40 }41 42 int main()43 {44 pthread_t tid[NUM];45 for (int i 0;i NUM;i)46 { 47 char tname[64];48 snprintf(tname,64,thread-%d,i 1);49 //多传入一个参数用来在创建线程执行相应任务所加到的对应的数字50 ThreadData* tp new ThreadData(tname,i 1,time(nullptr),100 4*i);51 pthread_create(tid i,nullptr,thread_run,tp);52 sleep(1);53 }54 55 void* ret nullptr;56 for (int i 0;i NUM;i)57 {58 int n pthread_join(tid[i],ret);59 //errno变量只有一个而线程有多个作同时修改可能会互相影响60 if(n! 0) cerr pthread_join error endl;61 ThreadData* tp static_castThreadData* (ret);62 if (tp-_status OK)63 {64 cout thread name: tp-_name 计算的结果为: tp-_result [0, tp-_top ] endl;65 }66 delete tp;67 }68 cout all thread quit endl;69 return 0;70 }
输出的结果如下图所示
让线程获取自己的线程id
那线程有自己的编号能不能让线程获取对应自己的编号呢 pthread库中也提供了相应的接口pthread_self 函数参数是没有的直接调用即可输出当前线程的id是什么 我们可以编写一段程序来看看对应的线程id同时返回到主线程也打印出来对比一下 1 #include iostream2 #include unistd.h3 #include pthread.h4 #include string5 #include ctime6 #define NUM 107 using namespace std;8 9 void* thread_create(void* args)10 {11 const char* name static_castconst char*(args);12 int cnt 5;13 while(cnt--)14 {15 cout name is running... obtain my tid: pthread_self() endl; 16 sleep(1);17 }18 19 pthread_exit((void*)11);20 }21 int main()22 {23 pthread_t tid;24 pthread_create(tid,nullptr,thread_create,(void*)thread 1);25 26 void* ret nullptr;27 int n pthread_join(tid,ret);28 if (n ! 0) cerr thread_join error: endl;29 cout new thread exit: (uint64_t)ret endl;30 cout quit thread id: tid endl;31 return 0;32 }
可以看到线程的id通过pthread_self函数是能够成功获取的
分离线程
前面我们提到主线程会阻塞等待新线程退出 但是阻塞等待也就意味着在这期间主线程并不能干任何事情 那假如我们想要主线程不阻塞等待让新线程自己回收自己又应该怎么操作呢 pthread库中实际上确实提供类似的接口函数pthread_detach 默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏 但假如我们不关心线程的返回值join是一种负担此时我们就可以修改对应线程的属性让线程自己退出时自动释放资源 PS假如一个线程分离detach后此时就不能再join了函数会发生报错 即joinable和分离是冲突的一个线程不能既是joinable又是分离的 下面我们简单写一段代码来验证joinable和分离两者是冲突的这个结论 1 #include iostream2 #include pthread.h3 #include unistd.h4 #include cstring5 #include string6 using namespace std;7 void* threadRoutine(void* args)8 {9 string name static_castconst char*(args);10 int cnt 5;11 while(cnt)12 {13 cout name : cnt-- endl;14 sleep(1);15 }16 return nullptr;17 }18 int main()19 {20 pthread_t tid;21 pthread_create(tid,nullptr,threadRoutine,(void*)thread 1);22 pthread_detach(tid); 23 int n pthread_join(tid,nullptr);24 if(0 ! n)25 {26 cerr error: n : strerror(n) endl;27 }28 return 0;29 30 }
我们再创建一个新线程后再detach掉对应的新线程 可以看到运行结果显示程序会直接发生报错 但除了在主线程进行detach外也可以在新线程中让新线程自己detach 比如说下面的代码 1 #include iostream2 #include pthread.h3 #include unistd.h4 #include cstring5 #include string6 using namespace std;7 void* threadRoutine(void* args)8 {9 pthread_detach(pthread_self());10 string name static_castconst char*(args);11 12 int cnt 5;13 while(cnt)14 {15 cout name : cnt-- endl;16 sleep(1);17 }18 return nullptr;19 }20 int main()21 {22 pthread_t tid;23 pthread_create(tid,nullptr,threadRoutine,(void*)thread 1);24 int n pthread_join(tid,nullptr); 25 if(0 ! n)26 {27 cerr error: n : strerror(n) endl;28 }29 return 0;30 31 }
但是运行的结果却并不符合预期我们并没有看到报错可以发现新线程照样可以正常跑 这是为什么呢 原因就在于我们刚开始说的线程谁先执行并不确定线程可能被创建出来但是并没有运行 在上述的代码中就是如此新线程虽然被创建了但是并没有被允许 此时主线程检测新线程的属性可以发现仍然是joinable的然后允许相关的join代码主线程被挂起此时新线程才被执行 因此假如我们要让新线程自己释放自己的资源的话我们还需要先让主线程sleep上对应的秒数让新线程先执行 此时允许的结果就符合我们之前的说法了 正是由于这个的缘故因此我们一般分离线程采取的方式都是建议主线程直接detach而不是自detach