C++&Qt经验总结【四】

踩坑日志(四)

目录

C++

深入理解iostream

紧紧接着上一次iostream的讨论

C++&Qt经验总结【三】_学艺不精的Антон的博客-CSDN博客

putback
  • 流函数istream::putback是通过调用basic_streambuf<>::sputbackc实现,basic_streambuf<>::sputbackc如果插入失败会调用保护虚函数basic_streambuf<>::pbackfail

  • **详解 sputback 与 pbackfail **

    • int_type sputbackc( char_type c );

      由basic_istream::unget 和 basic_istream::putback 调用

      当放回的字符等于上一次读取的字符,也就是说 c == *gptr() 时,指针gptr会左移,实现putback的操作,如果不相等会调用 pbackfail来进行插入。另外读取区为空时也调用pbackfail() (gptr() <= eback())

    • 这里使用之前的示例,并添加对pbackfail的重写

    • #include <iostream>
      #include <streambuf>
      using namespace std;
      class InBuffer:public streambuf{
          // 本缓存区对象,通过ostream 插入数据
      protected:
          char* buffer = nullptr;
          size_t buffer_size = 16;
          
          void dataMove(char* new_memory,size_t new_size){
              // 将当前buffer中的数据拷贝到new_memory中,删除原来的空间然后重置buffer_size 为 new_size
              for(int i = 0;i<buffer_size;++i){
                  new_memory[i] = buffer[i];
              }
          }
          void extendSpace(){
              char* new_memory = new char[this->buffer_size*2];
              this->dataMove(new_memory,this->buffer_size*2);
              size_t read_pos = this->gptr()-this->eback();
              delete this->buffer;
              this->buffer = new_memory;
              
              // 写入缓存区,指针重定向(比如之前的末尾是第7位,那么写入缓冲区应该从8开始)
              this->setp(this->buffer+buffer_size,this->buffer+2*buffer_size);
              // 读取缓存区指针重定向(有效的读取末尾应该是pptr 即当前的写入点)
              this->setg(this->buffer,this->buffer+ read_pos,this->pptr());
              this->buffer_size *= 2;
          }
      public:
          InBuffer(){
              this->buffer = new char[buffer_size];
              // 写入缓存区,指针重定向
              this->setp(this->buffer,this->buffer+16);
              // 读取缓存区指针重定向
              this->setg(this->buffer,this->buffer,this->pptr());
          }
          virtual ~InBuffer(){
              delete this->buffer;
          }
          virtual int_type pbackfail(int_type c)override{
              // 向控制台输出一条信息,标记pbackfail被调用
              cout<<"FAILED!"<<endl;
              return EOF;			// 标记流的结束,之后流的操作都无效
          }
          virtual int_type overflow(int_type c)override{
              // 溢出时,调用extendSapce扩展内存空间
              // 将读取缓存区的末尾指针与写入缓存区的末尾指针重新绑定
              this->extendSpace();
              // 因为触发了溢出,c是没有在内存中的,此时需要把他放进内存
              this->sputc(c);
              cout<<"Reset pointers!"<<endl;
              cout<<"Total size: "<< this->epptr()-this->eback()<<endl;
              return c;
          }
          virtual int_type underflow()override{
              cout<<"underflow called!"<<endl;
              if(this->gptr()>=this->pptr()){
                  // 可读的缓冲区已经耗尽,只能等待可读区扩展后才能继续读取
                  cout<<"RUN OUT"<<endl;
                  return EOF;
              }
              // 读取缓存区指针重定向(扩展读取上限)
              this->setg(this->eback(),this->gptr(),this->pptr());
              return traits_type::to_int_type(*gptr());
          }
          void print_ptr(){
              cout<<"WritePos:"<<(void*)this->pptr()<<endl;
              cout<<"["<<(void*)this->pbase()<<", "<<(void*)this->epptr()<<"]"<<endl;
              cout<<"ReadPos:"<<(void*)this->gptr()<<endl;
              cout<<"["<<(void*)this->eback()<<", "<<(void*)this->egptr()<<"]"<<endl;
          }
          void print_buffer(){
              for(int i = 0;i<this->buffer_size;++i){
                  cout<<this->buffer[i];
              }
              cout<<endl;
          }
      };
      
    • 与上一次读取的字符不相等时调用。

    • int main(){
          InBuffer buf;
          ostream out(&buf);
          istream in(&buf);
          char a;
          char b = 'N';
          out<<"dddd";
          
          in>>a;
          cout<<a<<endl;
          in.putback('c');
          in>>b;
          cout<<b;
          cout<<endl;
          
          return 0;
      }
      /*
      underflow called!
      d
      FAILED!
      N
      */
      
    • 读取缓存区没有空间供给字符的放回

    • int main(){
          InBuffer buf;
          ostream out(&buf);
          istream in(&buf);
          char a = 'N';
          in.putback('c');
          in>>a;
          cout<<a;
          
          return 0;
      }
      /*
      FAILED!
      N
      */
      
  • 重写pbackfail使得上面示例中的缓冲区可以放回不同的字符

  • virtual int_type pbackfail(int_type c)override{
        if(c != EOF){
            if(this->gptr()>this->eback()){
                // 有位置可放回
                this->gbump(-1);        // 将gptr移动到上一个位置
                *(this->gptr()) = c;
            }
            // EOF 会导致后续任何对istream的操作都无效,因此我们不主动返回EOF
        }
        return c;
    }
    
  • int main(){
        InBuffer buf;
        ostream out(&buf);
        istream in(&buf);
        char a = 'S';
        char b = 'N';
        out<<"dddd";
        in.putback('c');			// 没有空间放回,放回失败,不做操作
        in>>a;
        cout<<a<<endl;
        in.putback('c');			// 放回成功
        in>>b;
        cout<<b;
        cout<<endl;
        
        return 0;
    }
    
    /*
    d
    c
    */
    
缓冲区的清除

通过调用flush刷新缓冲区,其内部通过调用虚函数 sync()实现。

  • 其中sync()成功刷新应该返回0,失败返回-1,基类版本返回0

  • 根据上面的示例给出sync的重写版本,实现缓冲区的刷新(等价于重新初始化)

  • virtual int sync()override{
        // 将输出缓存区的起始点设置为buffer的首地址,即允许从头写入
        // 将输如缓冲区清空;
        cout<<"Buffer Flushed!"<<endl;
        this->setp(this->buffer,this->buffer + this->buffer_size);
        this->setg(this->buffer,this->buffer,this->pptr());
        return 0;
    }
    
    
  • 使用flush刷新缓冲区:

  • int main(){
        InBuffer buf;
        ostream out(&buf);
        istream in(&buf);
        out<<"abcdefg";
        buf.print_ptr();
        out<<flush;
        cout<<endl;
        buf.print_ptr();
        return 0;
    }
    /*
    WritePos:0x17d94342547
    [0x17d94342540, 0x17d94342550]
    ReadPos:0x17d94342540
    [0x17d94342540, 0x17d94342540]
    Buffer Flushed!
    
    WritePos:0x17d94342540
    [0x17d94342540, 0x17d94342550]
    ReadPos:0x17d94342540
    [0x17d94342540, 0x17d94342540]
    */
    // sync成功通过控制器flush刷新
    

    综上我们已经基本实现了类似strstreambuf的一个缓冲类

并发编程

  • 这里不再介绍线程和进程的概念,仅仅给出实践应用的示例,意在帮助初学者快速建立并发的思想。
基于进程的并发(创建进程)
  • Linux系统下使用C语言的标准库,unistd.h,(注意Windows下不可以使用该函数,Windows中需要调用Windows API), 以下示例基于 WSL Ubuntu 22.04 运行

    • 使用函数 fork 创建进程,并根据不同的pid区分代码所属的进程,做对应的工作

    • 定义一个基类,并由其派生出每个进程需要执行的工作:

    • class ProcessWorker{
       public:
           virtual void work()=0;
       };
       
       class FatherProccessWorker:public ProcessWorker{
       public:
           virtual void work()override{
               cout<<"father_proccess  ";
           }
       };
       class ChildProccessWorker:public ProcessWorker{
       public:
           virtual void work()override{
               cout<<"Child_proccess  ";
           }
       };
      
    • 创建新进程,这个过程会将main中的代码复制到两个进程中同时开始执行,其中父进程中fork函数返回0,子进程中fork()返回大于0的数,表示这个子进程的pid

    • int main(){
           pid_t ret_pid = fork();
           ProcessWorker* worker = nullptr;
           if(ret_pid < 0){
               return -1;
           }
           else if(ret_pid > 0){
               // 即当前进程是父进程
               worker = new FatherProccessWorker;
           }
           else{
               // 当前进程是子进程
               worker = new ChildProccessWorker;
           }
           for(int i = 0;i<100;++i){
               worker->work();
               cout<<i<<endl;
           }
           delete worker;
           return 0;
       }
      
    • 执行的结果中可以看到两个进程是交替执行的,这是因为控制台是一个临界资源,如果需要在一段时间内独占,应该为临界区的代码上锁,后面的进程、线程同步会介绍到

  • 使用exec族的函数

    • exec族函数包含以下几个成员

    • 返回值函数名参数列表
      intexeclconst char* path,const char* arg,…
      intexeclpconst char* file, const char* arg,…
      intexecleconst char* path, const char* arg,…,char* const envp[]
      intexecvconst char *path, char *const argv[]
      intexecvpconst char *file, char *const argv[]
      intexecveconst char *path, char *const argv[], char *const envp[]
    • 使用exec函数,实现进程替换

    • 首先编写一个外部程序:

    • #include <iostream>
      using namespace std;
      int main(int argc,char* argv[]){
          cout<<"External Proccess:"<<endl;
          cout<<"Args From call:"<<endl;
          for(int i = 0;i<argc;++i){
              cout<<argv[i]<<endl;
          }
          return 0;
      }
      
      
    • 编写调用处的程序

    • #include <iostream>
      #include <unistd.h>
      using namespace std;
      int main(int argc,char* argv[]){
          execl("externalEXE","hello","boys", NULL); //传入两个参数
          cout << "------------------\n";//如果execl执行成功,进程就被替换成了externalEXE,这一句不会执行
          
          return 0;
      }
      /*输出
      External Proccess:
      Args From call:
      hello
      world
      */
      
使用fork创建进程时的变量
  • 前面提到fork会将父进程中的变量复制到子进程中,以下分别讨论子进程对 局部变量全局变量 是否是同一个变量
#include <iostream>
#include <unistd.h>
#include <wait.h>
#include <semaphore>
using namespace std;
int glob_var = 0;
int main(){
    binary_semaphore sm(1);
    int loc_var_1 = 0;
    pid_t cur_pid = fork();
    int loc_var_2 = 0;
    if(cur_pid<0){
        cerr<<"Failed!"<<endl;
    }
    else if(cur_pid == 0){
        sm.acquire();
        cout<<"子进程:"<<endl;
        cout<<"Address glob_var "<<&glob_var<<"  Value:  "<<glob_var<<endl;
        cout<<"Address loc_var_1 "<<&loc_var_1<<"  Value:  "<<loc_var_1<<endl;
        cout<<"Address loc_var_2 "<<&loc_var_2<<"  Value:  "<<loc_var_2<<endl;
        glob_var += 3;
        loc_var_1 += 3;
        loc_var_2 += 3;
        sm.release(1);
        
    }
    else{
        sm.acquire();
        cout<<"父进程:"<<endl;
        cout<<"Address glob_var "<<&glob_var<<"  Value:  "<<glob_var<<endl;
        cout<<"Address loc_var_1 "<<&loc_var_1<<"  Value:  "<<loc_var_1<<endl;
        cout<<"Address loc_var_2 "<<&loc_var_2<<"  Value:  "<<loc_var_2<<endl;
        ++glob_var;
        ++loc_var_1;
        ++loc_var_2;
        sm.release(1);
    }
    wait(NULL);
    // 最终值(一共执行两次,分别是子进程输出以后执行的内容,和父进程执行完执行的内容)
    cout<<endl<<"FINISHED"<<endl;
    cout<<"Address glob_var "<<&glob_var<<"  Value:  "<<glob_var<<endl;
    cout<<"Address loc_var_1 "<<&loc_var_1<<"  Value:  "<<loc_var_1<<endl;
    cout<<"Address loc_var_2 "<<&loc_var_2<<"  Value:  "<<loc_var_2<<endl;
    return 0;
}
/*
父进程:
Address glob_var 0x561d7bea0274  Value:  0
Address loc_var_1 0x7ffe8dc79c6c  Value:  0
Address loc_var_2 0x7ffe8dc79c70  Value:  0
子进程:
Address glob_var 0x561d7bea0274  Value:  0
Address loc_var_1 0x7ffe8dc79c6c  Value:  0
Address loc_var_2 0x7ffe8dc79c70  Value:  0

FINISHED
Address glob_var 0x561d7bea0274  Value:  3
Address loc_var_1 0x7ffe8dc79c6c  Value:  3
Address loc_var_2 0x7ffe8dc79c70  Value:  3

FINISHED
Address glob_var 0x561d7bea0274  Value:  1
Address loc_var_1 0x7ffe8dc79c6c  Value:  1
Address loc_var_2 0x7ffe8dc79c70  Value:  1
*/
  • 可知虽然父进程和子进程的变量地址是相同的,但是子进程和父进程的值互不干扰。(信号量机制除外)
基于线程的并发(创建线程)
  • 使用标准库 (C++11)thread实现线程

    • 包含头文件: #include

    • 实现线程分为以下几个步骤:

      • 创建线程对象
      • 激活线程
  • 创建线程对象,线程对象可以接收函数指针,也可以接受重载过运算符()的对象

    • 接收函数指针

      • void func(int times){
            for(int i = 0;i<times;++i){
                cout<<"回调函数执行 "<<i<<endl;
            }
        }
        
      • // 创建线程对象,并添加线程的入口函数指针以及参数:
        thread th1(func,100);
        // 阻塞调用处的线程,直到this指针(th1)所指的线程执行完毕
        th1.join();
        
    • 接收对象

      • //可调用对象
        class FuncObj{
        public:
            void operator()(int times){
                for(int i = 0;i<times;++i){
                    cout<<"可调用对象执行 "<<i<<endl;
                }
            }
        };
        
      • // 实例化可调用对象
        FuncObj obj;
        // 创建线程对象,并添加入口可调用对象以及参数
        thread th2(obj,100);
        // 激活线程
        th2.join();
        
    • 完整示例:

      • #include <iostream>
        #include <thread>
        using namespace std;
        class FuncObj{
        public:
            void operator()(int times){
                for(int i = 0;i<times;++i){
                    cout<<"可调用对象执行 "<<i<<endl;
                }
            }
        };
        void func(int times){
            for(int i = 0;i<times;++i){
                cout<<"回调函数执行 "<<i<<endl;
            }
        }
        int main(){
            FuncObj obj;
            thread th1(obj,30);
            thread th2(func,30);
            th1.join();				// 阻塞主线程,直到th1执行完毕
            th2.join();				// 阻塞主线程,直到th2执行完毕
            return 0;
        }
        /*部分输出
        
        可调用对象执行 0
        可调用对象执行 1回调函数执行 0
        回调函数执行 1
        回调函数执行 2
        回调函数执行 3
        回调函数执行 4
        回调函数执行 5
        回调函数执行 6
        回调函数执行 7
        回调函数执行 8
        回调函数执行 9
        回调函数执行 10
        回调函数执行 11
        回调函数执行 12
        回调函数执行 13
        回调函数执行 14
        回调函数执行 15
        回调函数执行 16
        */
        
    • 其它常用函数

    • 函数原型解析
      std::thread::id get_id() const noexcept;返回线程id
      native_handle_type native_handle();返回POSIX标准的线程对象,即操作系统原生的线程句柄
      void join();等待对应对象的线程执行完毕
      void detach();分离对应对象的线程(僵尸线程一节会详解线程分离)
      void swap(thread& other);与other对象进行线程的交换
  • linux下使用专用API实现多线程

    • 参考: C++ 多线程 | 菜鸟教程 (runoob.com)

    • 包含头文件 #include <pthread.h>

    • 与标准库的不同,

      • 创建进程对象 pthread_t
      • 调用 pthread_create 函数创建并执行进程
      • 调用 pthread_exit 终止,
    • 使用pthread_t 创建线程对象(实际上是 typedef unsigned long),可以接收tid

    • pthread_t th
      
    • 使用 pthread_create 创建线程

      • pthread_create 的参数如下:

      • pthread_create(thread,attr,start_rountine,arg);
        
      • 返回值:成功则返回0,否则返回错误编号

      • 参数描述
        thread指向线程标识符指针。(即一个 pthread_t 对象)
        attr一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
        start_routine线程运行函数起始地址,一旦线程被创建就会执行。
        arg运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。
  • 可以使用pthread_exit显式地退出当前线程,pthread_exit的函数原型如下:

    • void* pthread_exit(void*);

    • 其中参数接收一个void* 型的指针,用于在调处所属的线程结束时返回该指针,在其它线程中可以通过pthread_join函数获取退出线程的返回值。pthread_join 具有阻塞作用,会阻塞当前线程直到需要获取返回值的线程执行结束。

    • 示例

    • #include <pthread.h>
      #include <unistd.h>
      #include <iostream>
      using namespace std;
      void* func(void* args){
          sleep(2);						// 休眠两秒钟,强制退出,并返回"Normall"
          cout<<"hello world"<<endl;
          string* ret_ptr = static_cast<string*>(args);
          *ret_ptr = "Normally";
          pthread_exit(ret_ptr);         // 显式地退出当前函数所在线程
          return ret_ptr;
      }
      int main(){
          pthread_t th;
          string ret;
          pthread_create(&th,NULL,func,&ret);
          void* ret_ptr;
          pthread_join(th,&ret_ptr);		// 等待线程 th执行完毕,回收其资源
          cout<<"Working thread terminated with return "<< *(string*)ret_ptr<<" "<<ret<<endl;
          return 0;
      }
      /*输出
      hello world
      Working thread terminated with return Normally Normally
      */
      
  • 创建线程推荐使用标准库,因为可以跨平台,不过在GCC11下 pthread 是可以在Windows下运行的另外pthread相对于标准库的std::thread 使用起来要复杂的多。

僵尸进程与僵尸线程

僵尸进程
  • 定义

    • UNIX(与类UNIX系统,如Linux) 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他,而父进程还没有结束(没有先于它结束),那么他将变成一个僵尸进程。
    • 如果一个进程的父进程已经 先于它 结束,那么它将由系统进程 init 来接管,init进程会自动wait其子进程,因此被 init 接管的进程不会变成僵尸线程
  • 性质

    • 僵尸进程放弃了几乎所有的内存空间,没有任何可执行的代码,也不可以被调度,仅仅在进程列表中保留一个位置(记录进程的退出状态等信息供给其它进程收集),除此之外僵尸进程不占有任何内存空间。
  • 危害

    • 如果一个父进程始终不使用wait回收其子进程的资源,并且不断地请求创建新的子进程,那么僵尸进程会越来越多,我么已经提到僵尸进程是占用 pid 资源的,如果过不清除掉僵尸进程那么pid的资源会越来越少,直到某一刻无法再创建新的子进程。
  • 实践(基于WSL2 Ubuntu22.04):Linux下创建一个僵尸进程,并通过指令查看僵尸进程 ps -ajx。

    • #include <unistd.h>
      #include <iostream>
      using namespace std;
      int main(){
          pid_t ret_pid;
          ret_pid = fork();
          if(ret_pid < 0){
              cout<<"Fail to create a process!"<<endl;
          }
          else if(ret_pid > 0){
              
              sleep(2);          // 睡眠2s,保证子进程在父进程之前结束
              cout<<"Here is Father process!: "<<getpid()<<endl;
              cout<<"The Child process is: "<<ret_pid<<endl;
              cout<<"The Grandpa process!: "<<getppid()<<endl;	// 运行该程序的终端程序
              // 不使用wait,不回收子进程的资源
              while(true);
          }
          else{
              cout<<"Here is Child process!: "<<getpid()<<endl;
              cout<<"The Child process is a zombie proccess now!"<<endl;
          }
      }
      
      
    • 僵尸进程测试1

      • 只需要看第 1 列 ppid (parent proccess identification) ,第2列 pid (proccess identification) ,第 7 列 status 进程状态,以及左后一列 进程的名称
      • 其中R表示运行
      • 其中Z表示僵尸状态
    • 程序输出:

    • root@Antona-Laptop:/AntonaLinuxCodes/test1/build# /AntonaLinuxCodes/test1/build/test_1
      Here is Child process!: 28381	
      The Child process is a zombie process now!
      Here is Father process!: 28380
      The Child process is: 28381
      The Grandpa process!: 28362
      
    • 其中我们的父进程 28380 的父进程(Grandpa process)是命令处理器bash 28362

    • 子进程 28381 处于僵尸状态

    • 关闭bash以后,Father Proccess 会被bash 杀掉,然后回收资源(自动wait),Child Proccess 28381 成为孤儿进程,将被 进程init 领养,然后回收资源,

      • 僵尸进程测试2

      • 可以看到 bash 28362, Father Process 28380, Child Process 28381,都被回收掉了。

  • **补充知识: **

    • init 是Linux系统下所有进程的父进程,其进程号pid为1,init 是Linux 内核引导运行的,是系统中的第一个进程
    • 僵尸进程是Unix进程模型的一个特性,因为在一个进程退出的时候不会发送退出状态到任何地方,相反只是等待直到其它进程查询到它的退出状态,而在Windows中使用系统API CreateProcess 创建进程时,可以指定退出状态发送的位置,Windows有自己的一套进程资源的回收机制,不会产生僵尸进程
    • 当一个父进程先于它的子进程噶掉,此时其子进程被称为孤儿进程,由 init 善后,一般来说由于init的自动wait操作,孤儿进程不会变成僵尸进程,并且僵尸进程可以随着其父进程被噶掉后变成孤儿进程由init回收资源。
wait 和 waitpid的使用办法

参考 进程同步:wait()和waitpid()函数_如果想让子进程先执行,父进程后执行,利用wait()/waitpid()函数怎样修改程序实现。_代码蛀虫向品的博客-CSDN博客

wait函数
  • 概述:

    • 头文件:#include<sys/wait.h>
  • 函数原型:

    • pid_t wait(int status);*
  • 函数参数说明:

    • status 是 int 类型的指针,用于保存进程退出时的状态信息
  • 作用说明:

    • 等待一个子进程,子进程结束后返回这个进程的pid
  • 示例:

#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;
int main(){
    pid_t ret_pid;
    ret_pid = fork();
    if(ret_pid < 0){
        
        cout<<"Fail to create a process!"<<endl;
    }
    else if(ret_pid > 0){
        
        
        cout<<"Here is Father process!: "<<getpid()<<endl;
        cout<<"The Child process is: "<<ret_pid<<endl;
        cout<<"The Grandpa process!: "<<getppid()<<endl;	// 运行该程序的终端程序
        // 使用wait等待子进程,并在子进程结束以后打印
        pid_t killed_child = wait(NULL);
        cout<<killed_child<<" dead !"<<endl;
        
    }
    else{
        sleep(2);          // 睡眠2s,保证可以观察到wait的阻塞作用
        cout<<"Here is Child process!: "<<getpid()<<endl;    }
}

Here is Father process!: 13977
The Child process is: 13978
The Grandpa process!: 13861
Here is Child process!: 13978
13978 dead !
  • 2s 后子进程执行完毕,父进程才开始执行
waitpid函数
  • 为什么需要waitpid函数,请看以下一个示例:
// 使用fork函数生成三个子进程
#include <unistd.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;
int main(){
    pid_t ret_pid = 0;
    
    for(int i = 0;i<3;++i){
        ret_pid = fork();
        if(ret_pid == 0){
            // 子进程放行
            break;
        }
    }
    if(ret_pid < 0){
        // 创建失败
        cerr<<"Fail to create proccess"<<endl;
        exit(2);
    }
    else if(ret_pid == 0){
        // 处理子进程的
        cout<<"There is Child "<<getpid()<<endl;
    }
    else{
        // 父进程,分别等待pid
        pid_t p1 = wait(NULL);
        pid_t p2 = wait(NULL);
        pid_t p3 = wait(NULL);
        cout<<p1<<" terminated!"<<endl;
        cout<<p2<<" terminated!"<<endl;
        cout<<p3<<" terminated!"<<endl;
    }
    return 0;
}

  • 第一次执行
There is Child 16530
There is Child 16531
There is Child 16529
16530 terminated!
16529 terminated!
16531 terminated!
  • 第二次执行
There is Child 16643
There is Child 16645
There is Child 16644
16645 terminated!
16644 terminated!
16643 terminated!
  • 执行的顺序和结束的顺序不同,没有规律,这和系统的调度和任务的复杂性都有关系。
  • 这里就体现出wait函数的缺陷了,由于wait是一旦等待到某个子进程就直接返回,它并不在意第一个结束的子进程是谁。

waitpid函数可以指定需要等待的进程pid

  • 函数原型:

    • pid_t waitpid(pid_t pid, int* status, int options);

  • 参数说明:

    • **参数pid **
      • 作用:指定要等待谁,如何等待。
        • pid > 0, 指定等待进程号为pid的子进程
        • pid = -1, 此时waitpid的效果与wait一致
        • pid = 0, 等待同一进程组的所有子进程(除非所有进程都结束,否则不返回)。
        • pid < -1, 等待指定指定进程组(pid的绝对值)中任意一个子进程和进程组。
    • 参数options
      • 指定waitpid的行为,比如是否要返回,什么情况下返回等,由由两个整型宏构成,可以通过按位或运算符同时选择
      • WONHANG 即时指定等待的pid没有结束也直接返回,不阻塞父进程
      • WUNTRACED 如果子进程暂停执行,直接返回
    • 返回值
      • 正常结束,返回等待到的子进程 pid
      • 调用出错返回-1
      • options的值为WONHANG时,没有已经退出的子进程可以收集,返回0;
    • 相关的示例详见参考链接,这里不再赘述。
僵尸线程
  • 前言:互联网上相关资料确实没有僵尸线程的明确定义,一般来说僵尸线程是根据线程分离的概念和僵尸进程的概念联系起来的一个概念

    • 僵尸进程是由于父进程没有给子进程收尸而造成的,相应地我们会考虑如果父线程没有给子线程收尸是否可能造成类似的问题,因此pthread_join就是类似wait用来给子线程收尸的。
  • 线程分离

    • 即在创建的时候将线程回收的工作交给系统来处理,类似父进程创建了子进程将其**”卖“**给init来处理。pthread_join其实增加了资源泄露的风险,开发者需要始终注意子线程的资源回收问题,因此除非特别在意线程退出的返回值,否则不如直接将子线程的回收权交给系统,既方便又安全。

    • pthread库下,可以使用以下API:

      • int pthread_detach(pthread_t tid);

    • 示例

      • 在创建的时候分离

      • #include <pthread.h>
        #include <unistd.h>
        #include <iostream>
        using namespace std;
        void* thread_func(void*){
            cout<<"Child thread!"<<endl;
            return nullptr;
        }
        int main(){
            pthread_attr_t attr;        // 线程的属性结构体
            pthread_attr_init(&attr);   // 初始化属性结构体
            pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); // 设置分离属性
        
            pthread_t tid;              // 新建一个变量用于存储线程号
        
        
            pthread_create(&tid,&attr,thread_func,nullptr);     // 为线程加入属性后创建线程
            sleep(2);           // 保证子线程先结束
            cout<<"Father thread!"<<endl;
            return 0;
        }
        
      • 在线程内部进行分离

      • #include <pthread.h>
        #include <unistd.h>
        #include <iostream>
        using namespace std;
        void* thread_func(void*){
            pthread_detach(pthread_self());
            cout<<"Child thread!"<<endl;
            return nullptr;
        }
        int main(){
            pthread_t tid;
            pthread_create(&tid,nullptr,thread_func,nullptr);
            sleep(2);           // 保证子线程先结束
            cout<<"Father thread!"<<endl;
            return 0;
        }
        
  • 补充知识:

    • 主线程就是进程本身

进程,线程控制(同步机制)

信号量详解
定义
  • 信号量用于描述资源的数量,只有资源数满足条件(大于0),一个线程或进程才可以访问某个临界资源。
互斥量与信号量
  • 互斥量和信号量区别趋势是一个十分令人困扰的问题

  • 信号量和互斥量都是用于线程同步的工具,但它们的实现和使用方式有所不同。

    1. 实现方式:

    互斥量是一种二元锁,只有两种状态:已锁定和未锁定,通过对互斥量加锁和解锁来实现线程的互斥访问。

    信号量是一种计数器,可用于控制同一时间内访问某个资源的线程数。信号量有一个初始值,每当一个线程访问该资源时,信号量的值就会减一;当线程访问结束时,信号量的值就会加一。

    1. 使用方式:

    互斥量只能用于两个线程之间的互斥访问,保证同一时间只有一个线程能够访问共享资源。

    信号量可以用于多个线程之间的同步和互斥访问,通过设置信号量的值来控制线程的访问。

    1. 应用场景:

    互斥量适用于任何需要在不同线程之间同步访问共享资源的情况,如多线程文件读写、多线程数据访问等。

    信号量适用于需要控制线程数量的场景,如线程池、生产者消费者模型等。

    总的来说,互斥量适用于对共享资源的互斥访问,而信号量适用于对线程数量的控制和同步。

C++20信号量实践
  • 直到C++20,标准库才开始支持信号量(使用原子操作实现),C++20之前只能使用互斥量mutex和条件判断来实现。使用的时候需要设置一下C++标准

    • 我使用的是VSCode+CMake:
      1. 首先Ctrl+,唤出设置窗口,搜索C++Standard 将C_CPP>Default: Cpp Standard 调成c++20,这样编辑区的自动提示就调出来了
      2. 修改CMakeLists.txt 添加一句 set(CMAKE_CXX_STANDARD 20) 这样编译器GCC就会知道使用哪种C++标准
  • 实践代码如下,创建两个线程,使用counting_semaphore使其互斥循环输出文本

  • counting_semaphore是一个模板类,可以通过模板参数设置信号量的最大资源数。

    • template<ptrdiff_t __least_max_value = __semaphore_impl::_S_max>

      class counting_semaphore;

  • PV操作详解,信号量最初是由荷兰学者 Edsger Dijkstra 提出的,P表示荷兰语中的 Passeren 相当于英语的 pass,表示消耗一个信号量,V表示荷兰语中的Verhoog 相当于英语 increment,表示释放一个信号量。

  • counting_semaphore 中实现P操作的是成员函数 aquire,其函数签名如下:

    • void

      acquire() noexcept(noexcept(_M_sem._M_acquire()))

      { _M_sem._M_acquire(); }

    • M_sem是一个原子对象,struct std::__atomic_semaphore

    • 剩余的信号量必须大于0才可以通过,否则会被aquire阻塞。

  • try_aquire可以实现非阻塞的请求消耗信号量,当没有信号量可以使用将返回false,让程序做某些操作

    • bool

      try_acquire() noexcept(noexcept(_M_sem._M_try_acquire()))

      { return _M_sem._M_try_acquire(); }

  • release 用于实现V操作,具有一个默认参数,默认释放一个信号量

    • release(ptrdiff_t __update = 1) noexcept(noexcept(_M_sem._M_release(1)))

      { _M_sem._M_release(__update); }

#include <thread>
#include <iostream>
#include <semaphore>
using namespace std;
counting_semaphore sm(0);           // 初始化信号量为0
void th1_works(int times){
    sm.acquire();
    for(int i = 0;i<times;++i){
        cout<<"Thread 1 :"<<i<<endl;
    }
    sm.release(1);
}

void th2_works(int times){
    sm.acquire();
    for(int i = 0;i<times;++i){
        cout<<"Thread 2 :"<<i<<endl;
    }
    sm.release(1);
}
int main(){
    thread th1(th1_works,100);
    thread th2(th2_works,120);
    sm.release(1);              // 释放一个信号量,获取到信号量的线程开始工作,进入临界区
    th1.join();
    th2.join();
}
  • Linux下使用fork创建两个进程,使用信号量binary_semaphore实现互斥的循环输出。

  • binary_semaphore实际上是由counting_semaphore实现的,源码中有这样一句:

    • using binary_semaphore=std::counting_semaphore<1>;

#include <iostream>
#include <wait.h>
#include <semaphore>
using namespace std;

class ProcessWorker{
public:
    virtual void work()=0;
    virtual ~ProcessWorker(){
        ;
    }
};

class FatherProcessWorker:public ProcessWorker{
public:
    virtual void work()override{
        cout<<"father_process  ";
    }
    ~FatherProcessWorker(){
        cout<<"The Father process worker destructed!"<<endl;
    }
};
class ChildProcessWorker:public ProcessWorker{
public:
    virtual void work()override{
        cout<<"Child_process  ";
    }
    ~ChildProcessWorker(){
        cout<<"The Child process worker destructed!"<<endl;
    }
};
binary_semaphore sm(1);
int main() {
    
    
    pid_t cur_pid = fork();
    ProcessWorker* worker = nullptr;
    
    if(cur_pid<0){
        cerr<<"Create Process Failed!"<<endl;
    }
    else if(cur_pid == 0){
        // 子进程
        worker = new ChildProcessWorker;
    }
    else{
        // 父进程
        worker = new FatherProcessWorker; 
        
    }
    sm.acquire();
    for(int i = 0;i<200;++i){
        worker->work();
        cout<<i<<endl;
    }
    delete worker;
    sm.release(1);
    wait(NULL);     // 等待子进程防止僵尸化
    return 0;
}
互斥量详解
  • 从C++11开始,标准库开始支持互斥量实现同步操作。

参考 C++——互斥量_c++ 互斥量_孟小胖_H的博客-CSDN博客

mutex互斥量
  • mutex互斥量是独占互斥量,不支持递归式给同一个std::mutex上锁(也就是说同一个线程多次给同一个互斥量上锁(一般是递归调用时上锁)可能导致死锁
mutex成员函数

void lock()

  • 锁定当前mutex对象,如果当前mutex对象已经被其它的线程或进程锁住,则会阻塞,直到锁被释放

void unlock()

  • 解锁(释放)当前mutex对象,这意味着临界区代码的结束。

bool try_lock()

  • 尝试锁定当前mutex对象,锁定失败时返回false,但不会阻塞调用处的线程或进程。
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
std::mutex m;
void th1_task(int times){
    m.lock();
    for(int i = 0;i<times;++i){
        cout<<"thread 1 :"<<i<<endl;
    }
    m.unlock();
}
void th2_task(int times){
    m.lock();
    for(int i = 0;i<times;++i){
        cout<<"thread 2 :"<<i<<endl;
    }
    m.unlock();
}
int main(){
    std::thread th1(th1_task,100);
    std::thread th2(th2_task,120);
    th1.join();
    th2.join();
    return 0;
}
与mutex搭配使用 lock_guard 与 unique_lock
  • 由于C++中引入了异常处理机制try_catch,因此存在一种情况,即在unlock之前抛出异常,mutex将无法被解锁,这样可能会造成死锁。
  • 因此引入lock_guard 与 unique_lock 对mutex进行包装,这两个对象一被创建就会对接收的mutex对象上锁,当前生存周期结束以后就会对mutex对象解锁,避免了因为异常机制而导致的无法及时解锁互斥锁的问题。
  • 其中unique_lock可以在其生命周期内调用lock和unlock进行上锁和解锁,不需要等待其析构。
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
std::mutex m;
void th1_task(int times){
    unique_lock<mutex> lck(m);
    for(int i = 0;i<times;++i){
        cout<<"thread 1 :"<<i<<endl;
    }
    
}
void th2_task(int times){
    lock_guard<mutex> lck_g(m);
    for(int i = 0;i<times;++i){
        cout<<"thread 2 :"<<i<<endl;
    }

}
int main(){
    std::thread th1(th1_task,100);
    std::thread th2(th2_task,120);
    th1.join();
    th2.join();
    return 0;
}
递归互斥锁recursive_mutex

递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:
(1)需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁;
(2)递归锁比起非递归锁,效率会低
(3)递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误。

递归互斥锁这里就不再赘述,相关演示可以查看前面的链接

带超时的互斥量 std::timed_mutex,std::recursive_timed_mutex
  • 这两种锁在超过一定锁定时间以后就会释放,一定程度上可以解决死锁带来的影响

相关演示可以查看前面的链接

自旋锁

参考 c++之理解自旋锁_c++自旋锁_DZGNB的博客-CSDN博客

定义

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

我之前编写的项目,在处理用户输入和线程读取(生产者消费者模型)时使用的就是自旋锁。当时对并发的知识了解较少,因此就使用了最简单的同步方式——自旋锁。

项目二:基于Qt的线性代数运算工具: 基于Qt开发,可以进行一些矩阵的运算 (gitee.com)

自旋锁是使用循环来实现阻塞的,因为需要不断检查条件因此会占用大量的CPU资源

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学艺不精的Антон

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值