UNP-UNIX网络编程 第二十六章:多线程

fork是昂贵的(把父进程的内存映像复制到子进程),并且需要进程间通信(IPC)机制。
线程的创建速度快(10-100 倍),同一进程中的线程共享相同的全局内存,线程之间容易共享信息,但是,这就带来了同步的问题。

同一进程内的所有线程除了共享全局变量,还共享:
进程指令,大多数数据,打开的文件(即文件描述符),信号处理函数和信号处置,当前工作目录,用户ID和组ID,”这些很容易出错误”

不过每个线程有各自的:线程ID,优先级,栈(用于存放局部变量和返回地址),寄存器集合(包括程序计数器和栈指针),errno,信号掩码。

(一)POSIX线程pthread:

1. 线程初始化:

//pthread_create()类似fork()
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void*), void *arg);//成功返回0,出错则为整的Exxx值

注:每个线程都有许多属性(attribute):线程ID,优先级,初始栈大小,是否应该成为一个守护线程,等,通常情况下我们采纳默认设置,把attr参数指定为NULL
func是该线程执行的函数,arg是该函数的参数,如果是多个参数,就需要打包成一个结构。

2. 等待线程结束:pthread_join()类似waitpid(可以不指定id,令ID=-1,来等待任意id的进程)

int pthread_join(pthread_t *tid, void **status);//成功返回0,出错则为整的Exxx值,必须指定tid

通过调用pthread_join等待一个给定的线程终止。如果status指针非空,来自所等待线程的返回值(一个指向某个对象的指针)将存入由status指向的位置。

3. 获取自身ID:pthread_self类似getpid

pthread_t pthread_self();//返回调用线程的线程ID

4 .脱离线程:pthread_detach

int pthread_detach(pthread tid);//成功返回0,出错则为整的Exxx值

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

pthread_detach(pthread_self());//让自己脱离

5.pthread_exit函数:让一个线程终止

void pthread_exit(void *status)

对于可汇合的线程,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join(),
另外两个终止线程的方法;
(1)启动线程的函数(creat的第三个参数)返回
(2)进程的main函数返回,或者线程调用了exit

6. 多线程客户端

#include    "unpthread.h"//unp.h和包裹函数们Pthread
void    *copyto(void *);
static int  sockfd;     /* global for both threads to access */
static FILE *fp;
void str_cli(FILE *fp_arg, int sockfd_arg)
{
    char        recvline[MAXLINE];
    pthread_t   tid;

    sockfd = sockfd_arg;//线程外部变量,套接字描述符
    fp = fp_arg;//和标准I/O FILE指针

    Pthread_create(&tid, NULL, copyto, NULL);//创建线程,新线程id返回到tid,
//copyto从标准输入读到EOF先于main函数终止。子线程在执行copyto同时,主线程就去执行下面了。
    while (Readline(sockfd, recvline, MAXLINE) > 0)//主线程循环
        Fputs(recvline, stdout);//文本行从套接字到标准输出
}//终止进程,进程内所有线程也被终止

void* copyto(void* arg)//子线程
{
    char    sendline[MAXLINE];

    while (Fgets(sendline, MAXLINE, fp) != NULL)//标准输入拷贝到套接字,
        Writen(sockfd, sendline, strlen(sendline));

    Shutdown(sockfd, SHUT_WR);//当在标准输入读到EOF时,调用shutdown从套接字送出FIN

    return(NULL);//终止子线程
        /* 4return (i.e., thread terminates) when EOF on stdin */
}

7. 多线程服务器

#include    "unpthread.h"

static void *doit(void *);      /* each thread executes this function */

int main(int argc, char **argv)//每个客户使用一个线程
{
    int             listenfd, //*iptr;
    pthread_t       tid;
    socklen_t       addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");

    cliaddr = Malloc(addrlen);

    for ( ; ; ) {//使用Pthread_create代替fork
        len = addrlen;
        iptr = Malloc(sizeof(int));//每次循环都开辟新的空间,不会用到全局的值
        int *iptr = Accept(listenfd, cliaddr, &len);
        Pthread_create(&tid, NULL, &doit, iptr);//函数doit和参数-已连接套接字描述符connfd,最后一个参数传值(指针拷贝)
    }
}

static void* doit(void* arg)//子线程arg就是参数iptr
{
    int connfd = *((int*)arg);//void* -> int*,然后再解引用
    free arg;//arg已经完成任务了
    Pthread_detach(pthread_self());//自身脱离,主线程没有理由等待他所创建的子线程
    str_echo(connfd);   /* same function as before */
    Close(connfd);//线程关闭,已连接套接字不一定被Close;在进程中,子进程结束,套接字就close了
    return(NULL);
}

8. 给新线程传递参数

把connfd强制转换为void并不保证一定起作用,也不能直接把connfd的地址传递给新线程

9. 线程安全:图 26-5 线程安全版本

gethostbyname 和 gethostbyaddr不可重入,但是有可重入的_r版本,要注意的是,这不是标准,所有不可冲入的函数要慎用(表 11-21)

10. 线程特定数据

当把非线程的传旭写成线程的版本时,会碰到函数中使用静态变量的情况,从而引起常见的编译错误。在不考虑冲入的情况下,静态变量无可非议,但是在同一个进程中的不同线程几乎同时调用这样的函数,就会出现错误,后果是不确定的,因为这些静态变量无法为不同的线程保存各自的值。
解决方法:
(1)使用线程特定数据,这样转换成了只能在多线程系统上工作的函数。
每个系统支持有限的线程特定数据元素。POSIX要求这个限制不小于128(每个进程)。系统为每个进程维护一个我们称之为key结构的结构数组
key结构中的标志指示这个数据元素是否正在使用,所有的标志初始化为“不在使用”。当一个线程调用pthread_key_create创建一个新的线程特定数据元素时,系统会返回第一个不在使用的元素。key结构中的析构函数指针,当一个线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。
除了进程范围的key结构数组外,系统还在进程内维护关于每个线程的多条信息。这些特定于线程的信息我们称之为pthread结构,其部分内容是我们称之为pkey数组的一个128个元素的指针数组。
注意当我们调用pthread_key_create创建一个键时,系统告诉我们这个键。每个线程可以随后为该键存储一个值(指针),而这个指针通常是每个线程通过malloc获得的。具体的函数如下:

#include <pthread.h>  
int pthread_once(pthread_once_t *onceptr, void (*init)(void));  
int pthread_key_create(pthread_key_t *keyptr, void (*destructor)(void *value));  
void *pthread_getspecific(pthread_key_t key);  
int pthread_setspecific(pthread_key_t key, const void *value);
不管多少个线程只有第一个执行它的线程运行一次被掉函数,保证了分配的rl_key的安全。 
if ((ptr = pthread_getspecific(rl_key)) == NULL) // 检查当前线程key域是否有值
pthread_setspecific(rl_key, ptr);// 设置当前线程的pthread结构key的值  
void destructor(void *value) 
{  
    printf("pthread:%d destructor value(%p)\n", pthread_self(), value);  
    free(value);// 线程结束后执行。线程结束后,会自动掉此析构函数,释放分配资源。 
}  

(2)改变调用顺序,把函数参数和静态变量都放在一个结构中,可以再支持/不支持线程的系统上使用,但是调用函数的所有应用程序都要更改。
(3)改变接口结构,避免使用静态变量

扫描二维码关注公众号,回复: 406325 查看本文章

(二)互斥锁:开销并不大

父子进程除了描述符外不共享其余的任何东西。
多个线程更改一个共享变量,解决方法是使用一个互斥锁,保护这个共享变量,访问该变量的条件是持有互斥锁

int pthread_mutex_lock(pthread_mutex_t *mptr);//上锁
int pthread_mutex_unlock(pthread_mutex_t *mptr);//解锁
//成功返回0,失败返回正值Exxx值

如果试图上锁一个已经被另外一个线程锁住的一个互斥锁,本线程被阻塞,知道该互斥锁被解锁。
如果某个互斥锁变量是静态分配的,我们就必须把它初始化为常量PTHREAD_MUTEX_INITIALIZER。如果我们在共享内存区分配一个互斥锁,那么必须通过调用pthread_mutex_init函数在运行时把它初始化。
在每个子线程内:

pthread_mutex_t lock;  
pthread_mutex_lock(&lock);  
i++;  
Pthread_mutex_unlock(&lock); 

(三)条件变量
互斥锁用于防止多个线程同时访问某个变量,但我们还需要在等待某个条件(事件)发生期间能让我们进入睡眠的机制。
如果没有这个机制,线程在等待一个条件发生期间只能轮询,这显然非常浪费CPU资源。条件变量加上互斥锁就能实现这种机制。条件变量的类型是pthread_cond_t。条件变量API如下:

/*等待一个条件变量,线程进入睡眠状态*/  
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);  
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr,  
   const struct timespec *abstime);//时刻而不是时间  

/*唤醒等待在条件变量上的一个线程*/  
int pthread_cond_signal(pthread_cond_t *cptr);  
/*唤醒等待在条件变量上的所有线程*/  
int pthread_cond_broadcast(pthread_cond_t *cptr);  

举个例子,我们使用一个全局变量flag标志一个事件是否发生。线程A测试flag如果为0,表明事件未发生则睡眠等待。线程B产生这个事件然后将flag标志置1,唤醒线程A。为此我们定义了以下三个变量:

int flag;  //计数器
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  //互斥锁
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;  //条件变量
线程A使用如下的代码等待事件发生:
pthread_mutex_lock(&mutex);  
while (flag == 0)  
    pthread_cond_wait(&cond, &mutex); /*睡眠等待事件发生*///解锁,cond调用signal之后加锁
    /*下一步的动作*/  
pthread_mutex_unlock(&mutex);  
线程B使用如下的代码产生事件并唤醒线程A:
/*某个事件发生*/  
pthread_mutex_lock(&mutex);  
flag = 1;  
pthread_cond_signal(&cond); /*唤醒线程A*/  
pthread_mutex_unlock(&mutex);  

条件变量总是和一个表示“条件”的全局变量关联,在此例中即是flag变量,flag值为0表明条件不成立(事件还未发生)。
全局变量总是需要互斥锁保护,因此互斥锁和条件变量经常一起使用。这也解释了为什么pthread_cond_wait函数的两个参数一个是条件变量一个是互斥锁。
另外,由于测试条件之前总是先加锁,所以当条件不成立时pthread_cond_wait函数必须先解锁,然后把调用线程投入睡眠。
当线程被唤醒时,它又再次加锁,然后返回。

猜你喜欢

转载自blog.csdn.net/qiangzhenyi1207/article/details/79079505