『 Linux 』线程控制

2 篇文章 0 订阅
2 篇文章 0 订阅


线程库

请添加图片描述

在Linux内核中没有实际的线程概念,只有轻量级进程的概念,即使用task_struct内核数据结构模仿线程;

所以本质上在Linux内核中无法直接调用系统调用接口创建线程,只能调用系统调用接口创建轻量级进程;

在Linux上需要对线程进行控制需要使用第三方线程库,即<pthread.h>,该库封装了Linux下的轻量级进程接口;

同时该库也成为了Linux平台下的默认库,即Linux系统将会自带该库;

该库为动态库,封装了一系列的线程控制接口,当使用该库创建线程时其对应的线程的结构将被该动态库进行组织与维护;

该库为第三方库,所以在使用该库时需要使用-lpthread动态链接该库;

该库中使用了大量的void*类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括对象;


线程的创建

请添加图片描述

线程的创建通常使用pthread_create()接口;

NAME
       pthread_create - create a new thread

SYNOPSIS
       #include <pthread.h>

       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

       Compile and link with -pthread.

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

当函数调用成功时返回0,调用失败时返回!0;

  • pthread_t *thread

    该参数为一个输出型参数,类型为pthread_t*,其中pthread_t类型本质上是一个对unsigned long int类型的typedef;

    typedef unsigned long int pthread_t;
    

    该输出型参数用于返回线程threadid;

  • const pthread_attr_t *attr

    该参数用于指向线程属性结构的指针,如果为nullptr则使用默认属性;

    可以用来设置线程的各种属性,如栈的大小,调度策略等,通常情况下使用nullptr默认情况即可;

  • void *(*start_routine) (void *)

    该参数为一个函数指针,用于传递一个函数;

    创建的新线程将会使用该回调函数,接收的参数为一个void*的类型的参数并返回void*类型的返回值;

    同时该函数为新线程的入口点;

  • void *arg

    传递给start_routine函数的参数,若是不需要传递参数则设置为nullptr;

该函数中众多的void*类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括类;

  • void*类型的大小

    void*void两个类型一个是空指针类型一个是空类型,两种类型都具有大小;

    其中void*为指针,大小与所有指针相同,在不同机器位的机器下的大小都不同;

    void的大小在Linux中一般为1;

    int main()
    {
      printf("sizeof(void) = %d\n", sizeof(void));
      printf("sizeof(void*) = %d\n", sizeof(void*));
      return 0;
    }
    

    结果为:

    $ ./mythread 
    sizeof(void) = 1
    sizeof(void*) = 8
    

    不同的是void*可以用来声明变量,void不能声明变量;

pthread_create()使用如下:

void* Print(void* arg) {
  while (true) {
    cout << "I am "<<(const char*)arg<<" , the PID : " << getpid() << endl;
    sleep(2);
  }
  return nullptr;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
  sleep(1); // 确保线程已经被运行

  while (true) {
    cout << "I am main thread , the PID : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

定义一个pthread_t类型的变量用于接收该新线程的返回id;

使用pthread_create()函数创建一个线程并传入一个字符串作为回调函数的参数并强转为(void*)以方便传参;

同时设置了一个名为Print()函数作为该线程的入口点;

新线程所用函数与主线程相同都对自身的PID进行打印;

使用-lpthread编译后可使用ldd进行验证该库是否为第三方库;

$ ldd mythread
	linux-vdso.so.1 =>  (0x00007ffe6f966000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6598b80000) # 已链接pthread库
	libstdc++.so.6 => /home/__USR/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f65987ff000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f65984fd000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f65982e7000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f6597f19000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f6598d9c000)

在新窗口中使用shell脚本对进程进行监控;

$ while :; 
do ps axj | head -1 && ps axj | grep mythread | grep -v grep ; 
echo "----------------------------" ; 
sleep 1 ; 
done

执行结果为如下:

# 程序所在窗口
$ ./mythread 
I am new_thread1 , the PID : 6974
I am main thread , the PID : 6974
...

# shell脚本所在窗口
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread

其中脚本可以看出只有一个进程在运行,且进程中的两个线程作为不同的执行流都打印同一个PID;

可使用ps -aL | grep <Processname>查看当前对应运行进程中的轻量级进程的基本属性(该命令为内置命令,且Linux中不存在线程概念,所以查看的属性为轻量级进程的基本属性);

$ ps -aL | head -1 && ps -aL |  grep mythread
  PID   LWP TTY          TIME CMD
 6974  6974 pts/0    00:00:00 mythread
 6974  6975 pts/0    00:00:00 mythread

其中LWPLight-weight process 轻量级进程 的缩写,对应的PIDLWP相同的轻量级进程即可看出是 “主线程” ;


线程库中的线程ID

请添加图片描述

pthread_create()函数中的*thread参数用于返回所创建线程的tid;

而实际上该参数所返回的是一个地址;

  • pthread_self()

    在线程中可使用pthread_myself()接口获取自身线程的tid;

    NAME
           pthread_self - obtain ID of the calling thread
    
    SYNOPSIS
           #include <pthread.h>
    
           pthread_t pthread_self(void);
    
           Compile and link with -pthread.
    
    DESCRIPTION
           The  pthread_self()  function  returns the ID of the calling thread.
           This  is  the  same  value  that  is  returned  in  *thread  in  the
           pthread_create(3) call that created this thread.
    
    RETURN VALUE
           This function always succeeds, returning the calling thread's ID.
    

    返回值为一个pthread_t类型的参数,即线程本身的tid;

void* Print(void* n) {
  printf("I am newthread , the tid:%p\n",pthread_self());
  sleep(1);
  return nullptr;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, nullptr);
  sleep(1);  // 确保线程已经被运行
  printf("the new thread tid : %p\n", newthread);
  sleep(3);
  return 0
}

该程序中创建了一个新线程,并且在新线程中使用pthread_self()获取自身线程tid并打印;

运行结果如下:

$ ./mythread 
I am newthread , the tid:0x7f18642ef700
the new thread tid : 0x7f18642ef700

主线程与新线程打印tid结果相同;

本质上是因为在Linux中线程是<pthread.h>线程库所提供的概念,当需要用到线程时需要将<pthread.h>库链接至内存共享区中;

线程的概念为<pthread.h>提供,故该库也应需要对线程进行组织与管理,使用<pthread.h>库所创建的线程(除主线程外)都将被该库在共享区中进行组织与管理;

故在Linux(CentOS7)中,本身pthread_t *thread参数为一个可以索引至共享区中对该应线程属性结构的线性地址(虚拟地址);

由于地址具有唯一性,该设计可以使得其TID既保证了能够进行唯一标识,也可以快速(直接或间接)访问到对应的TCB结构体当中;

一般来说该TCB结构体为了防止线程的安全用户无法直接访问,只能通过该库内部的机制进行间接访问;


线程等待及线程退出

请添加图片描述

线程与进程相同都需要被等待,否则将出现类似于僵尸进程一样的问题,即线程已经执行完毕但未被回收导致资源被占用;

通常主线程需要使用pthread_join()对新线程进行等待回收;

NAME
       pthread_join - join with a terminated thread

SYNOPSIS
       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

       Compile and link with -pthread.

RETURN VALUE
       On  success, pthread_join() returns 0; on error, it returns an error
       number.

该函数调用成功时返回0,失败时返回!0;

  • pthread_t thread

    该参数用于需要等待的一个线程ID,即调用该函数的前提是必须存在一个pthread库所创建的线程;

  • void **retval

    该参数用于返回线程所调用回调函数的返回值;

    以二级指针的方式对返回值进行接收;

    参数返回时不能直接返回线程栈上开辟空间的数据,因为线程的栈是其独立的,当程序执行完毕后表示该线程的生命周期结束,对应的其栈的空间将会被回收;

    如果直接返回线程栈中的数据则可能会出现段错误;

    若是不关心该线程的返回值或是线程无有用返回值可设置为nullptr;

void* Print(void* arg) {
  cout << "I am " << (const char*)arg << " , the PID : " << getpid() << endl;

  int* n = new int;
  *n = 42;
  cout << "new thread quit..." << endl;
  sleep(1);
  return n;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
  sleep(1);  // 确保线程已经被运行

  cout << "I am main thread , the PID : " << getpid() << endl;
  unsigned long int* ret;
  pthread_join(newthread, (void**)&ret);
  sleep(2);

  printf("new thread quit... , the exit_code:%lu\n", *ret);
  delete ret;

  return 0;
}

该程序中创建了一个线程,并在该线程的结束时返回一个通过new创建的一个数据(42)以避免直接返回线程栈中的数据;

主线程中创建了一个ret变量用于接收该线程执行完毕的返回值并对该返回值进行打印,调用pthread_join()等待该线程;

可使用shell脚本对线程进行观察:

$ while :;
do ps -aL | head -1 && ps -aL |  grep mythread ;
echo "------------------------------------" ; 
sleep 1 ;
done

结果如下:

# 程序所在会话
$ ./mythread 
I am new_thread1 , the PID : 8942
new thread quit...
I am main thread , the PID : 8942
	## 中间间隔了两秒 
new thread quit... , the exit_code:42


# shell脚本所在会话
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
 8942  8943 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
------------------------------------

运行结果为线程所调用的回调函数返回值被主线程接收,同时可以清楚看到新线程执行完毕后退出同时资源被回收;

主线程中间隔了两秒才打印最终结果表示该函数默认行为为阻塞等待;

  • 线程的退出

    线程的退出无法使用exit();

    本质原因是exit()的退出是调用了_exit()系统调用接口的,该系统调用接口直接会将进程进行退出,退出的是进程而不是线程;

    一般情况下线程的退出不仅可以使用return,也可以使用pthread_exit()函数;

    NAME
           pthread_exit - terminate calling thread
    
    SYNOPSIS
           #include <pthread.h>
    
           void pthread_exit(void *retval);
    
           Compile and link with -pthread.
    

    该函数为退出正在执行的线程;

    传递一个void*类型的参数以表示该线程的返回值,通常该值不能是线程栈上开辟的空间中的数据,必须是在非栈上这种共享空间中,因为该线程的生命周期已经结束,若是访问线程栈中的数据将会导致段错误;

    • pthread_cancel()

      该接口函数可以在主线程中取消一个对应tid的线程;

      NAME
             pthread_cancel - send a cancellation request to a thread
      
      SYNOPSIS
             #include <pthread.h>
      
             int pthread_cancel(pthread_t thread);
      
             Compile and link with -pthread.
      
      RETURN VALUE
             On success, pthread_cancel() returns  0;  on  error,  it  returns  a
             nonzero error number.
      

      参数为pthread_t thread类型表示需要传入一个该线程对应的标识符tid;

      当主线程使用该函数取消一个线程时对应的该线程会返回一个PTHREAD_CANCELED的宏,该宏的值默认为-1;

  • 线程的崩溃

    进程在退出时具有三种情况:

    • 程序执行完毕,结果正确
    • 程序执行完毕,结果错误
    • 程序未执行完毕异常退出

    线程不具备第三种情况,原因是异常结束本质上是收到了信号,而信号的概念是与进程相关的;

    这表明线程收到信号后就代表进程收到了信号;

  • 使用线程函数传入对象完成总数相加计算小程序

    该程序为计算startend相加的和;

    class Request {
     public:
      Request(int start, int end, const string &threadname)
          : start_(start), end_(end), threadname_(threadname) {}
    
     public:
      int start_;
      int end_;
      string threadname_;
    };
    
    class Response {
     public:
      Response(int result, int exitcode) : result_(result), exitcode_(exitcode) {}
    
     public:
      int result_;
      int exitcode_;
    };
    
    void *sumCount(void *args) {
      Request *rq = static_cast<Request *>(args); // static_cast<Request *>(args) 类比于 (Request*)args
      Response *ret = new Response(0, 0);
      for (int i = rq->start_; i <= rq->end_; ++i) {
        ret->result_ += i;
      }
      return ret;
    }
    
    int main() {
      pthread_t tid;
      Request *rq = new Request(1, 100, "Thread1");
      pthread_create(&tid, nullptr, sumCount, rq);
    
      Response *rsp;
      pthread_join(tid, (void **)&rsp);
    
      printf("the sumSount sucess , the result is %d , the exitcode is %d\n",
             rsp->result_, rsp->exitcode_);
      delete rsp;
      delete rq;
      return 0;
    }
    

    该程序设计了两个类,分别为计算数值传入的类Request,一个为返回对应计算结果与退出码的类Response;

    并设计了一个sumCount()函数作为线程的入口点也是用于计算的方法;


C++11 中的线程库

请添加图片描述

C++11已经支持了线程库;

在不同的平台下其封装的底层实现不同,在Linux下C++11的线程库主要是对pthread库的封装

这意味在Linux下使用C++11的线程库时仍需在编译时链接对应的pthread库;

void thread_cpp() {
  while (true) {
    cout << " I am a new thread from cpp" << endl;
    sleep(1);
  }
}

int main() {
  thread t(thread_cpp);
  t.join();  // 用于等待线程 对线程进行清理工作
  return 0;
}

运行结果为:

$ ./mythread 
 I am a new thread from cpp
 I am a new thread from cpp
 ...
^C

线程库的线程与轻量型进程

请添加图片描述

pthread线程库本质上就是封装了Linux系统关于轻量型进程的系统调用接口;

在Linux系统中创建轻量级进程的系统调用接口为clone();

NAME
       clone, __clone2 - create a child process

SYNOPSIS
       /* Prototype for the glibc wrapper function */

       #include <sched.h>

       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

       /* Prototype for the raw system call */

       long clone(unsigned long flags, void *child_stack,
                 void *ptid, void *ctid,
                 struct pt_regs *regs);

   Feature   Test   Macro   Requirements   for   glibc   wrapper   function  (see  fea‐
   ture_test_macros(7)):

       clone():
           Since glibc 2.14:
               _GNU_SOURCE
           Before glibc 2.14:
               _BSD_SOURCE || _SVID_SOURCE
                   /* _GNU_SOURCE also suffices */

RETURN VALUE
       On  success,  the  thread  ID  of  the child process is returned in the caller's
       thread of execution.  On failure, -1 is returned in  the  caller's  context,  no
       child process will be created, and errno will be set appropriately.

该函数用来创建一个新的进程(轻量型进程);

其中本质上fork()系统调用接口就是封装了该接口;

当该函数调用成功时将返回新轻量型进程的ID,失败时返回-1并设置errno;

  • int (*fn)(void *)

    该参数用于传递一个回调函数,该回调函数为该轻量型进程的入口点;

  • void *chile_stack

    这是一个指向子进程的栈底指针,新创建的这个进程(轻量型进程)将使用这个栈;

    通常着需要指向一个预先分配好的栈内存区域,并且栈的顶端应该对齐;

  • int flags

    这是一个标志位参数,用于指定新进程的行为和资源共享方式;

  • void *arg

    该参数用于传给fn的参数,子进程将以此为参数调用fn函数;

  • 可选参数

    pid_t *ptid,struct user_desc *tls,pid_t *ctid;

对应的pthread_create()则是封装了该函数;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Dio夹心小面包

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

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

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

打赏作者

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

抵扣说明:

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

余额充值