操作系统多进程编程、多线程编程
多线程:
一个多线程的简单实现:
https://blog.****.net/sujianwei1123/article/details/76183682
fork函数需要unistd.h的头文件,unistd.h头文件的接口通常都是大量针对系统调用的封装,是对类 Unix 系统。
getpid函数是获得进程号
sleep函数:执行挂起一段时间,也就是等待一段时间在继续执行。秒级别。
exit函数讲解:https://blog.****.net/love_gaohz/article/details/46667007 当进程执行到exit()或_exit()函数时,进程会无条件的停止剩下的所有操作,清除各种数据结构,并终止本进程的运行。每个exit函数也只关闭相对应的进程。
在操作系统的基本概念中进程是程序的一次执行,且是拥有资源的最小单位和调度单位(在引入线程的操作系统中,线程是最小的调度单位)。在Linux系统中创建进程有两种方式:一是由操作系统创建,二是由父进程创建进程(通常为子进程)。系统调用函数fork()是创建一个新进程的唯一方式,当然vfork()也可以创建进程,但是实际上其还是调用了fork()函数。fork()函数是Linux系统中一个比较特殊的函数,其一次调用会有两个返回值,下面是fork()函数的声明:
当程序调用fork()函数并返回成功之后,程序就将变成两个进程,调用fork()者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。从性能方面考虑,父进程到子进程的数据拷贝并不是创建时就拷贝了的,而是采用了写时拷贝(copy-on -write)技术来处理。调用fork()之后,父进程与子进程的执行顺序是我们无法确定的(即调度进程使用CPU),意识到这一点极为重要,因为在一些设计不好的程序中会导致资源竞争,从而出现不可预知的问题。
注意:for循环生成了5个pid,再加上父进程就是6个进程。刚开始以为for循环进程的生成,后面的pid会覆盖掉前面的pid,以为就生成了一个子进程,事实是生成5个子进程,不会覆盖
fork返回值为0,子进程;不为0,父进程
个人理解其实就是通过pid的编号来实现控制不同的进程的运行和终止。
#include<stdio.h> #include<unistd.h> #include <sys/types.h> #include<vector> #include <iostream> #include <stdlib.h> using namespace std; int main() { string sMatch; pid_t pid; vector<string> provList; provList.push_back("100"); provList.push_back("200"); provList.push_back("300"); provList.push_back("400"); provList.push_back("500"); cout<<"main process,id="<<getpid()<<endl; //循环处理"100,200,300,400,500" for (vector<string>::iterator it = provList.begin(); it != provList.end(); ++it) { sMatch=*it; pid = fork(); //子进程退出循环,不再创建子进程,全部由主进程创建子进程,这里是关键所在 if(pid==0||pid==-1) { break; } } if(pid==-1) { cout<<"fail to fork!"<<endl; exit(1); } else if(pid==0) { //这里写子进程处理逻辑 cout<<"this is children process,id="<<getpid()<<",start to process "<<sMatch<<endl; sleep(10); exit(0); } else { //这里主进程处理逻辑 cout<<"this is main process,id="<<getpid()<<",end to process "<<sMatch<<endl; exit(0); } return 0; }
Linux下一个进程在内存里有三部分的数据,就是”代码段”、”堆栈段”和”数据段”。接触过汇编语言的人了解,一般的CPU都有上述三种段寄存器,以方便操作系统的运行。这三个部分也是构成一个完整的执行序列的必要的部分。
“代码段”,顾名思义,就是存放了程序代码的数据,如果机器中有数个进程运行相同的一个程序,那么它们就可以使用相同的代码段。”堆栈段”存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc
之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据段。
有两个基本的操作用于创建和修改进程:函数fork()
用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝,利用了父进程的代码段、堆栈段、数据段,当父子进程中对共有的数据段进行重新设值或调用不同方法时,才会导致数据段及堆栈段的不同;函数族exec()
用来启动另外的进程以取代当前运行的进程,除了PID仍是原来的值外,代码段、堆栈段、数据段已经完全被改写了。
https://blog.****.net/sodino/article/details/45146001
http://www.doc88.com/p-9681830324447.html
多线程编程:
使用pthread.h的库函数
1.一个简单例子:
pthread_create:创建一个新的线程的函数,一旦创建就会执行这个函数
pthread_exit:用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用
注意:1.线程的运行函数在函数名前加了*号,因为pthread_create函数的第三个参数是线程运行函数起始地址
2.pthread_create返回值,创建线程成功时,函数返回 0;若返回值不为0则说明创建线程失败
3.使用 -lpthread 库编译
#include <iostream> #include <pthread.h> using namespace std; #define NUM_THREADS 5 // 线程的运行函数 void* say_hello(void* args) { cout << "Hello Runoob!" << endl; return 0; } int main() { // 定义线程的 id 变量,多个变量使用数组 pthread_t tids[NUM_THREADS]; for(int i = 0; i < NUM_THREADS; ++i) { //参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数 int ret = pthread_create(&tids[i], NULL, say_hello, NULL); if (ret != 0) { cout << "pthread_create error: error_code=" << ret << endl; } } //等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来; pthread_exit(NULL); }
每次的输出结果不一定一样:
2.给线程调用的函数传参数:
pthread_create函数的第四个参数就是可以传递给运行函数的参数
#include <iostream> #include <cstdlib> #include <pthread.h> using namespace std; #define NUM_THREADS 5 void *PrintHello(void *threadid) { // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取 int tid = *((int*)threadid); cout << "Hello Runoob! 线程 ID, " << tid << endl; pthread_exit(NULL); } int main () { pthread_t threads[NUM_THREADS]; int indexes[NUM_THREADS];// 用数组来保存i的值 int rc; int i; for( i=0; i < NUM_THREADS; i++ ){ cout << "main() : 创建线程, " << i << endl; indexes[i] = i; //先保存i的值 // 传入的时候必须强制转换为void* 类型,即无类型指针 rc = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i])); if (rc){ cout << "Error:无法创建线程," << rc << endl; exit(-1); } } pthread_exit(NULL); }
输出的结果:
两次的结果不一样,有“创建函数”几个字的行应该都是在运行主线程
3.线程调用的函数在一个类中,那必须将该函数声明为静态函数
因为静态成员函数属于静态全局区,线程可以共享这个区域,故可以各自调用
#include <iostream> #include <pthread.h> using namespace std; #define NUM_THREADS 5 class Hello { public: static void* say_hello( void* args ) { cout << "hello..." << endl; } }; int main() { pthread_t tids[NUM_THREADS]; for( int i = 0; i < NUM_THREADS; ++i ) { int ret = pthread_create( &tids[i], NULL, Hello::say_hello, NULL ); if( ret != 0 ) { cout << "pthread_create error:error_code" << ret << endl; } } pthread_exit( NULL ); }
4.
线程同步:一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才能执行。
线程互斥:一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,解锁。
线程互斥是一种特殊的线程同步。
Linux中 四种进程或线程同步互斥的控制方法:
1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
2、互斥量(Mutex):为协调共同对一个共享资源的单独访问而设计的。
3、信号量:为控制一个具有有限数量用户资源而设计。
4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
Linux下线程同步最常用的三种方法就是互斥锁、条件变量及信号量,互斥锁通过锁机制来实现线程间的同步,锁机制是同一时刻只允许一个线程执行一个关键部分的代码;条件变量是用来等待而不是用来上锁的,条用来自动阻塞一个线程,直到某特殊情况发生为止
互斥锁的一个实例https://blog.****.net/Return_nellen/article/details/79916519:
PTHREAD_MUTEX_INITIALIZER:锁的初始化,pthread_mutex_lock:加锁,pthread_mutex_unlock:释放锁
#include <iostream> #include <pthread.h> #include <stdio.h> #include <semaphore.h> #include <stdlib.h> #include <string.h> using namespace std; pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; int tf[5]; void* print(void* i) { pthread_mutex_lock(&mut); for(int j=0;j<3;j++) // cout << i << " " << j << endl; cout << j << endl; pthread_mutex_unlock(&mut); } int main() { pthread_t td[3]; for(int i=0;i<3;i++) tf[i] = i; for(int i=0;i<3;i++) pthread_create(&td[i],NULL,print,(void *)&tf[i]); for(int i=0;i<3;i++) pthread_join(td[i],NULL); pthread_mutex_destroy(&mut); }
运行结果:
如果不用互斥锁,也就是在运行函数那将锁的打开和关闭注释掉,运行结果如下:
可以看到加锁是一个线程一个线程的运行,输出的结果是有序的,不加锁就是乱序的
条件变量的实例:https://www.cnblogs.com/xudong-bupt/p/6707070.html
pthread_cond_wait:线程阻塞在条件变量
pthread_cond_signal:线程被唤醒
pthread的pthread_join函数https://blog.****.net/dinghqalex/article/details/42921931:
使用方式:创建线程之后直接调用pthread_join方法就行了
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。即pthread_join()的作用可以这样理解:主线程会阻塞等待子线程的终止。也就是在子线程调用了pthread_join()方法后面的代码,只有等到子线程结束了才能执行。
读写锁和互斥锁区别:
读写锁特点:
1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
互斥锁特点:
一次只能一个线程拥有互斥锁,其他线程只有等待
c++11有STL封装的thread类,它的前身据说是boost::thread
多进程多线程选择:https://blog.****.net/RUN32875094/article/details/79515384
1)需要频繁创建销毁的优先用线程
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
4)可能要扩展到多机分布的用进程,多核分布的用线程
原因请看上面对比。
https://zhuanlan.zhihu.com/p/37029560 python多线程多进程