网络编程学习——线程(一)

1 概述

  在传统的UNIX模型中,当一个进程需要另一个实体来完成某事时,它就fork一个子进程并让子进程去执行处理。UNIX上的大多数网络服务器程序就是这么编写的:父进程accept一个连接,fork一个子进程,该子进程处理与该连接对端的客户之间的通信。

  fork调用存在一些问题。

  • fork是昂贵的。fork要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符,如此等等。当今的实现使用称为写时复制(copy-on-write)的技术,用以避免在子进程切实需要自己的副本之前把父进程的数据复制到子进程。然而即使有这样的优化措施,fork仍然时昂贵的。

  • fork返回之后父子进程之间信息的传递需要进程间通信(IPC)机制。调用fork之前父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始运行。然而从子进程往父进程返回信息却比较费力。

  线程有助于解决这两个问题。线程有时成为轻权进程(lightweight process),因为线程比进程“权重轻些”。也就是说,线程的创建可能比进程的创建快10~100倍。

  同一进程内的所有线程共享相同的全局内存。这使得线程之间易于共享信息,然而伴随着这种简易性而来的却是同步(synchronization)问题。

  同一进程内的所有线程除了共享全局变量外还共享:

  • 进程指令

  • 大多数数据

  • 打开的文件(即描述符)

  • 信号处理函数和信号处置

  • 当前工作目录

  • 用户ID和组ID

  不过每个线程也有自己的:

  • 线程ID

  • 寄存器集合,包括程序计数器和栈指针

  • 栈(用于存放局部变量和返回地址)

  • errno

  • 信号掩码

  • 优先级 

2 基本线程函数:创建和终止

2.1 pthread_create函数

  当一个程序由exec启动执行时,成为初始线程(initial thread)或主线程(main thread)的单个线程就创建了。其余线程则由pthread_create函数创建。

#include<pthread.h>
/* Create a new thread, starting with execution of START-ROUTINE
   getting passed ARG.  Creation attributed come from ATTR.  The new
   handle is stored in *NEWTHREAD.  */
extern int pthread_create (pthread_t *__restrict __newthread,
               const pthread_attr_t *__restrict __attr,
               void *(*__start_routine) (void *),
               void *__restrict __arg) __THROWNL __nonnull ((1, 3));

  一个进程内的每个线程都由一个线程ID(thread ID)标识,其数据类型为pthread_t(往往是unsigned int)。如果新的线程成功创建,其ID就通过tid指针返回。

#include<bits/pthreadtypes.h>
/* Thread identifiers.  The structure of the attribute type is not
   exposed on purpose.  */
typedef unsigned long int pthread_t;

  每个线程都有许多属性(attribute):优先级、初始栈大小、是否应该成为一个守护线程,等等。我们可以在创建线程时通过初始化一个取代默认设置的pthread_attr_t变量指定这些属性。通常情况下我们采纳默认设置,这时我们把attr参数指定为空指针。

#include<bits/pthreadtypes.h>
union pthread_attr_t
{
  char __size[__SIZEOF_PTHREAD_ATTR_T];
  long int __align;
};

  创建一个线程时我们最后指定的参数是由该线程执行的函数及其参数。该线程通过调用这个函数开始执行,然后或者显示地终止(通过调用pthread_exit),或者隐式地终止(通过让该函数返回)。该函数的地址由func参数指定,该函数的唯一调用参数是指针arg。如果我们需要给该函数传递多个参数,我们就得把它们打包成一个结构,然后把这个结构的地址作为单个参数传递给这个起始函数。

  注意func和arg的声明。func所指函数作为参数接受一个通用指针(void*),又作为返回值返回一个通用指针(void*)。这使得我们可以把一个指针(它指向我们期望的任何内容)传递给线程,又允许线程返回一个指针(它同样指向我们期望的任何内容)。

  通常情况下Pthread函数的返回值成功时为0,出错时为某个非0值。与套接字函数及大多数系统调用出错时返回-1并置errno为某个正值的做法不同的是,Pthread函数出错时作为函数返回值返回正值错误指示。距离来说,如果pthread_create因在线程数目上超过某个系统限制而不能创建新线程,函数返回值将是EAGAIN。Pthread函数不设置errno。成功为0出错为非0这个约定不成问题,因为<sys/errno.h>头文件中所有的Exxx值都是正值。0值从来不被赋予任何Exxx名字。

 

2.2 pthread_join函数

  我们可以通过pthread_join等待一个给定线程终止。对比线程和UNIX进程,pthread_create类似于fork,pthread_join类似于waitpid。

#include <pthread.h>
/* Make calling thread wait for termination of the thread TH.  The
   exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN
   is not NULL.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pthread_join (pthread_t __th, void **__thread_return);

  我们必须指定要等待线程的tid。不幸的是,Pthread没有办法等待任意一个线程(类似指定进程ID参数为-1调用waitpid)。

  如果status指针非空,来自所等待线程的返回值(一个指向某个对象的指针)将存入由status指向的位置。

 

2.3 pthread_self函数

  每个线程都有一个在所属进程内标识自身的ID。线程ID由pthread_create返回,而且我们已经看到pthread_join使用它。每个线程使用pthread_self获取自身的线程ID。

#include <pthread.h>
/* Obtain the identifier of the current thread.  */
extern pthread_t pthread_self (void) __THROW __attribute__ ((__const__));

  对比线程和UNIX进程,pthread_self类似于getpid。

2.4 pthread_detach函数

  一个线程或者是可汇合的(joinable,默认值),或者时脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一个线程什么时候终止,那就最好保持第二个线程的可汇合状态。

  pthread_detach函数把指定的线程转变为脱离状态。

#include <pthread.h>
/* Indicate that the thread TH is never to be joined with PTHREAD_JOIN.
   The resources of TH will therefore be freed immediately when it
   terminates, instead of waiting for another thread to perform PTHREAD_JOIN
   on it.  */
extern int pthread_detach (pthread_t __th) __THROW;

  本函数通常想让自己脱离的线程调用,就如下语句:

pthread_detach (pthread_self());

 

2.5 pthread_exit函数

  让一个线程终止的方法之一是调用pthread_exit。

#include <pthread.h>
/* Terminate calling thread.

   The registered cleanup handlers are called via exception handling
   so we cannot mark this function with __THROW.*/
extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));

  如果本线程未曾脱离,它的线程ID和退出状态将一直保留到调用进程内的某个其他线程对它调用pthread_join。

  指针status不能指向局部于调用线程的对象,因为线程终止时这样的对象也消失。

  让一个线程终止的另一种两个方法是:

  • 启动线程的函数(即pthread_create的第三个参数)可以返回。既然该函数必须声明成返回一个void指针,它的返回值就是相应线程的终止状态。

  • 如果进程的main函数返回或者任何线程调用了exit,整个进程就终止,其中包括它的任何线程。

3 使用线程的str_cli函数

  图1-1展示了该函数线程版本的设计。

184503_h9Q6_2537915.jpg

图1-1 使用线程重新编写str_cli

  下面时使用线程的str_cli函数。

#include <pthread.h>

void *copyto( void * );

static int sockfd;
static FILE *fp;

void str_cli( FILE *fp_arg, int sockfd_arg )
{
  char recvline[ MAX_MESG_SIZE ];
  pthread_t tid;
  
  // 我们将要创建的线程需要str_cli的2个参数:fp(输入文件的标准I/O库FILE指针)和sockfd(连接到服务器的
  // TCP套接字描述符)。为了简单起见,我们把这2个参数值保存到外部变量中。另一个技巧是把这两个值放到一个结
  // 构中,然后把指向这个结构的一个指针作为参数传递给我们将要创建的线程。
  sockfd = sockfd_arg; // copy arguments to externals
  fp = fp_arg;
  
  // 创建线程,新线程ID返回到tid中。由新线程执行的函数是copyto。没有参数传递给该线程。
  pthread_create( &tid, NULL, copyto, NULL );
  
  // 主线程调用readline和fputs,把从套接字读入的每个文本行复制到标准输出
  while( readline( sockfd, recvline, MAX_MESG_SIZE ) > 0 )
    fputs( recvline, stdout );
  // 当str_cli函数返回时,main函数通过调用exit终止进程,进程内的所有线程也随之被终止。通常情况下,copyto
  // 线程在从标准输入读到EOF时已经先于main函数的exit调用终止。然而要是发生服务器过早终止之事,尚未读入EOF
  // 的copyto线程就得由main函数调用exit来终止
}

// 该线程只是把读自标准输入的每个文本行复制到套接字。当在标准输入上读得EOF时,它通过调用shutdown从套接字
// 送出FIN,然后返回。从启动该线程的函数return来终止该函数
void * copyto( void *arg )
{
  char sendline[ MAX_MESG_SIZE ];
  
  while( fgets( sendline, MAX_MESG_SIZE, fp ) != NULL )
    writen( sockfd, sendline, strlen( sendline ) );
    
    shutdown( sockfd, SHUT_WR ); // EOF on stdin, send FIN
    
    return( NULL ); // return ( i.e.,thread terminates ) when EOF on stdin
}

 

4 使用线程的TCP回射服务器程序

  下面给出了本服务器程序。

#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>  
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#define SERV_PORT 5566
#define MAX_MESG_SIZE 1024

using namespace std;

void mimiasd_echo( int connectfd );
static void * doit( void *arg );

int main()
{
  int sockfd, connectfd;
  pthread_t tid;
  struct sockaddr_in servaddr;
  bzero( &servaddr, sizeof( servaddr ) );
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons( SERV_PORT );
  servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
  
  // 创建一个TCP套接字
  if( ( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) 
  {
    printf( " socket error!\n " );
    return -1;
  }
  
  // 在待绑定到TCP套接字的网际套接字地址结构中填入通配地址(INADDR_ANY)和服务器的众所周知端口(SERV_PORT,
  // 这里定义为5566)。绑定通配地址是在告知系统:要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接。
  // 我们选择TCP端口号应该比1023大(我们不需要一个保留端口),比5000大(以免与许多源自Berkeley的实现分配临
  // 时端口的范围冲突),比49152小(以免与临时端口号的“正确”范围冲突),而且不应该与任何已注册的端口冲突。
  // listen把该套接字转换成一个监听套接字。 
  if( ( bind( sockfd, ( struct sockaddr* ) &servaddr, sizeof(servaddr) ) ) < 0 ) 
  {
    printf( " bind error!\n " );
    return -1;
  }
  
  if( listen( sockfd, 5 ) < 0 )
  {
    printf( " listen error!\n " );
    return -1;
  }

  // accept返回之后,改为调用pthread_create取代调用fork。我们传递给doit函数的唯一参数是已连接套接字描述
  /  符connectfd。
  for( ; ; )
  {
    // 服务器阻塞于accpet调用,等待客户连接的完成。第二和第三个参数都是空指针,说明对客户的身份不感兴趣。
    connectfd = accept( sockfd, NULL, NULL );
    pthread_create( &tid, NULL, &doit, ( void * ) connectfd );
  }
}

void mimiasd_echo( int connectfd )
{
  ssize_t n;
  char buff[ MAX_MESG_SIZE ];
  
  // recv函数从套接字读入数据,send函数把其中内容回射给客户。如果客户关闭连接(这是正常情况),那么接收到客
  // 户的FIN将导致服务器子进程的recv函数返回0,这又导致mimiasd_echo函数的返回,从而在上面main函数里阻止子
  // 进程。
  while( ( n = recv( connectfd, buff, MAX_MESG_SIZE, 0 ) ) > 0 )
    send( connectfd, buff, n, 0 );
  if( n < 0 )
    printf( " read error!\n " );
}

// doit是由线程执行的函数。线程首先让自身脱离,因为主线程没有理由等待它创建的每个线程。然后调用mimiasd_echo
// 函数。该函数返回之后,我们必须close已连接套接字,因为本线程和主线程共享所有的描述符。对于使用fork的情形,
// 子进程就不必close已连接套接字,因为子进程旋即终止,而所有打开的描述符在进程终止时都将被关闭。
static void * doit( void *arg )
{
  pthread_detach( pthread_self() );
  mimiasd_echo( ( int ) arg ); // same function as before
  close( ( int ) arg ); // done with connected socket 
  return( NULL );
}

 

4.1 给新线程传递参数

  我们前面把整形变量connectfd类型强行转化成void指针并不保证在所有系统上都能起作用。从ANSI C角度看这是可以接受的:ANSI C保证我们能把一个整数指针类型强制转化为void *,然后把这个(void *)指针强制转换回原来的整数指针。问题就出在这个整数指针指向什么上。

  主线程中只有一个整数变量connectfd,每次调用accept该变量都会被覆盖写以一个新值(已连接描述符)。因此可能发生下述情况。

  • accept返回,主线程把返回值(譬如说新的描述符是5)存入connfd后调用pthread_create。pthread_create的最后一个参数是指向connectfd的指针而不是connectfd的内容。

  • Pthread函数库创建一个线程,并准备调度doit函数启动执行。

  • 另一个连接就绪且主线程在新创建的线程开始运行之前再次运行。accept返回,主线程把返回值(譬如现在的描述符是6)存入connectfd后调用pthread_create。

  尽管主线程共创建了两个线程,但是它们操作的都是存放在connectfd中的最终值(我们假设是6)。问题出在多个线程不是同步地访问一个共享变量(以取得存放在connectfd中的整数值)。前面通过把connectfd的值(而不是指向该变量的一个指针)传递给pthread_create来解决本问题。按照C向被调用函数传递整数值的方式(把该函数的一个副本推入调用函数的栈中),这个解决办法是可行的。

  下面提供一个解决本问题的更好办法。

void mimiasd_echo( int connectfd );
static void * doit( void *arg );

int main()
{
  int sockfd, *iptr;
  pthread_t tid;
  struct sockaddr_in servaddr;
  bzero( &servaddr, sizeof( servaddr ) );
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons( SERV_PORT );
  servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
  
  if( ( sockfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) 
  {
    printf( " socket error!\n " );
    return -1;
  }
  
  if( ( bind( sockfd, ( struct sockaddr* ) &servaddr, sizeof(servaddr) ) ) < 0 ) 
  {
    printf( " bind error!\n " );
    return -1;
  }
  
  if( listen( sockfd, 5 ) < 0 )
  {
    printf( " listen error!\n " );
    return -1;
  }

  for( ; ; )
  {
    iptr = malloc( sizeof( int ) );
    *iptr = accept( sockfd, NULL, NULL );
    pthread_create( &tid, NULL, &doit, iptr );
  }
}

void mimiasd_echo( int connectfd )
{
  ssize_t n;
  char buff[ MAX_MESG_SIZE ];
  
  while( ( n = recv( connectfd, buff, MAX_MESG_SIZE, 0 ) ) > 0 )
    send( connectfd, buff, n, 0 );
  if( n < 0 )
    printf( " read error!\n " );
}

static void * doit( void *arg )
{
  int connectfd;
  
  connectfd = *( ( int * ) arg );
  free( arg );
  
  pthread_detach( pthread_self() );
  mimiasd_echo( connectfd ); // same function as before
  close( connectfd ); // done with connected socket 
  return( NULL );
}

 

 

 

 

 

转载于:https://my.oschina.net/u/2537915/blog/665855

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值