线程 一

怎么在单个进程中执行多个任务。一个进程中的所有线程都可以访问该进程的组成部件,例如文件描述符和内存。不管在什么情况下,只要单个资源需要在多个用户间共享,就必须处理一致性问题。我们将在之后的笔记中学习目前可用的同步机制,防止多个线程在共享资源时出现不一致问题。

线程的概念

典型的unix进程可以看成只有一个控制线程,即一个进程在某一时刻只能做一件事情。

有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能做不止一件事,每个线程处理各自独立的任务。

  • 通过为每种事件类型提供单独的处理线程,可以简化异步处理事件的代码。每个线程在进行事件处理时可以采用同步模式,相比较异步编程要简单的多。
  • 多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享,但是多线程可以自动的访问相同的存储地址空间和文件描述符。
  • 有些问题可以分解为多个线程来处理,这样可以提高程序的吞吐量。在单线场景中,要处理多个任务,那只能把这些任务串行化。但在多线程场景下,相互独立的任务处理就可以交叉进行,此时只需要为每个任务分配一个单独的线程,当然只有在两个任务的处理过程互不依赖的情况下两个任务才可以交叉执行。
  • 和系统交互的场景也可以使用多线程来改善响应时间。可以用单独的线程来处理输入输出,这样可以改善用户体验。
    现实场景中人们把多线程的程序设计与多处理器或者多核联系怎么起来?

但即使程序运行在单处理器上,也能得到多线程模型的好处。处理器的数量并不影响程序结构。所以不管处理器的个数多少,程序都可以通过使用线程得以简化,而且,即使多线程程序在串行化任务时不得不租塞,由于某种线程在阻塞的时候还由另外一些线程可以运行,所以,多线程在单处理器上的运行还是可以改善响应时间和吞吐量的。
每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。

线程标识

进程ID在整个系统中是唯一的,但是线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
进程ID使用pid_t是一个非负整数。线程ID是用pthread_t表示,但是不同系统中pthread_t使用的数据类型不同,因此这给程序的跨平台造成了阻碍,因此必须使用如下的函数来判断两个线程ID是否相等

#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
                                                                     // 若相等返回非零数值,否则,返回0

线程可以调用pthread_self(void)函数获得自身的线程ID。

#include <pthread.h>
pthread_t  pthread_self(void);                                       // 返回调用线程的线程ID

我们假设有一个场景:主线程把工作任务放在一个队列中,用线程ID来控制每个工作线程处理那些作业。
在这里插入图片描述
主线程把新任务放在任务队列中,三个线程组成的线程池冲队列中取出作业。主线程不允许每个线程随意的处理队列中的作业,而是用主线程控制作业的分配,主线程会在每个待处理任务的结构体中放置处理该任务的线程ID,每个工作线程只能取出表示有自己线程的ID的任务。而pthread_self可以取出每个线程的自身ID,用此ID和任务队列中的任务结构体中保存的线程ID进行匹配pthread_equal

线程的创建

首先,我们需要了解一下传统和POSIX这两种模型下的线程:
传统Unix进程模型,每个进程只有一个控制线程,也就是一个进程就是一个线程。在POSIX模型下,一个进程启动以后,它可以创建出多个线程。新增的线程可以通过pthread_create函数创建。

#include <pthread.h>
int pthread_create(pthread_t* restrict tidp,
                   const pthread_attr_t* restrict attr,
                   void* (*start_rtn)(void*),
                   void* restrict arg);
                   // 成功返回0, 不成功返回错误编码
  • 当pthread_create成功返回时,新创建的线程ID会被设置成tidp指向的内存单元。
  • attr: 参数用于定制各种不同的线程属性。这里我们暂且设置为NULL,表示默认属性的线程。
  • start_rtn: 是一个函数指针,它是新创建线程函数的开始地方。
  • arg: 是无类型指针参数,是向start_rtn函数传递的参数,如果传递的参数比较多,可以把参数放在一个结构体中,然后把结构体的地址作为arg参数传入。
  1. 线程创建时并不能保证哪个线程会先运行: 是创建的线程还是主线程。
  2. 这里有一个问题:那就是怎么处理主线程和新线程之间的竞争
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_t ntid;
void printids(const char* s){
   pid_t  pid;
   pthread_t  tid;
   pid = getpid();
   tid = pthread_self();
   printf("%s  pid = %lu  ,tid= %lu (0x%lx)\n",s,(unsigned long)pid,(unsigned long)tid, (unsigned long)tid);
}

void * thr_fn(void* arg){
  printids("new thread:");
  return ((void*)0);
}

int main(void){
   int err;
   err = pthread_create(&ntid, NULL ,thr_fn, NULL);
   if(err !=0 ){
     printf("create thread failed!\n");
     exit(0);
   }
   printids("main thread:");
   sleep(1);
   exit(0);
}

我们在程序中给主线程加了一个休眠,就是防止新线程还没有机会运行,整个进程可能已经终止了。这种问题主要看操作系统中线程的实现和调度算法。

  1. 第二个问题是新线程是通过pthread_self函数获取自己的线程ID的,而不是从共享内存中读取的,或者从线程的启动例程中以参数的形式接受到的,我们看pthread_create函数第一个参数,返回新线程的ID。主线程把新线程ID放在ntid中,但是线程并不能安全的使用它,原因是如果新线程在主线程调用pthread_create返回之前就运行了,那么新线程看到的是未经初始化的ntid。

我们一起来看一下运行结果:
linux下运行效果:

main thread:  pid = 9913  ,tid= 140148306085696 (0x7f76d2000740)
new thread:  pid = 9913  ,tid= 140148295403264 (0x7f76d15d0700)

FreeBSD运行结果:

main thread:  pid = 37396  ,tid= 673190208 (0x28201140)
new thread:  pid = 37396  ,ti=673280320 (0x28217140)

linux是返回的线程ID是无符号长整型,但看起来像指针。在一般的linux系统gdb查看线程的堆栈时,查找出问题的线程id,可以把这个无符号长整型转换成10进制整数,然后在日志中找到这个线程。
FreeBSD返回的是返回的是指向线程数据结构的指针,FreeBSD是使用它来表示线程ID的。

linux 2.4和linux2.6在线程实现上是不同的。有兴趣可以网上搜一下。

线程终止

  • 如果进程中的任意线程调用了exit、_Exit或者 _exit, 那么整个进程就会终止。
  • 不终止整个进程,单线程的终止有三种方法:
    • 线程从启动例程中返回,返回值是线程的退出码。
    • 线程可以被同一个线程中的其他线程取消。
    • 线程调用pthread_exit。
pthread_exit 函数
#include <pthread.h>
void pthread_exit(void* rval_ptr);

rval_ptr: 是一个无类型指针,与传给启动线程的单个参数类似。进程中的其他线程可以通过pthread_join来访问这个指针。
pthread_createpthread_exit函数的无类型指针参数可以传递的值不止一个,这个指针参数可以传递包含复杂的结构体地址,但是注意,这个结构所使用的内存在调用者完成调用以后必须任然有效。

pthread_join 函数
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
													   // 成功返回0,失败返回错误编号

如果有线程调用pthread_join,那么此线程将一直等待它指定的线程结束。

  • 如果指定的线程结束是通过正常的返回结束,那么rval_ptr就是返回码。
  • 如果线程是被取消,由rval_ptr指定的内存将设置为 PTHREAD_CANCELED

如果对线程的返回值并不感兴趣,那个可以设置rval_ptr为NULL。这种情况下,调用pthread_join函数可以等待指定的线程终止,但并不获取线程的终止状态。

下面的代码我们来获取已终止的线程的退出码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thr_fn1(void *arg){
   printf("thread 1 returning\n");
   return ((void*)1);
}
void * thr_fn2(void *arg){
   printf("thread 2 exiting\n");
   pthread_exit((void*)2);
}

int main(){
  int err;
  pthread_t tid1,tid2;
  void *tret;
  err = pthread_create(&tid1,NULL,thr_fn1,NULL);
  if(err != 0 )
          printf("can't create thread 1: %d\n",err);

  err = pthread_create(&tid2,NULL,thr_fn2,NULL);
  if(err != 0 )
          printf("can't create thread 2: %d\n",err);

  err = pthread_join(tid1 ,&tret);
  if(err != 0 )
       printf("can't join with thread1:%d\n",err);
  printf("thread 1 exit code %ld\n", (long)tret);

  err = pthread_join(tid2 , &tret);
  if( err != 0 )
       printf("can't join with thread 2 :%d\n", err);
  printf("thread 2 exit code %ld\n", (long)tret);

return 0;
}

输出结果:
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2
pthread_cancel 函数

线程可以通过调用pthread_cancel函数来请求取消 同一进程中 的其他线程。

#include <pthread.h>
int pthread_cancel(pthread_t tid);
												 // 成功返回0,失败返回错误编码

在默认情况下,pthread_cancel函数会使得由tid标识的线程的行为表现 如同调用了参数为PTHREAD_CANCELED的pthread_exit函数,但是,线程可以选择忽略取消或者控制如何被取消。
线程退出时也可以安排退出时需要调用的函数,这个和进程调用atexit一样。

#include <pthread.h>
void pthread_cleanup_push(void (*rtn) (void*), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数rtn是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:

  • 调用pthread_exit时;
  • 响应取消请求时;
  • 用非零execute参数调用pthread_cleanup_pop时;

如果execute参数设置为0,清理函数将不被调用。
不管发生上述那种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理程序。
这些函数有一个限制,由于它们可以实现为宏,所以必须在与线程相同的作用域中以匹配对的形式使用。
pthread_cleanup_push的宏定义可以包含字符,这种情况下,在pthread_cleanup_pop的定义中要有对应的匹配字符。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值