UNIX 多线程编程入门

本文介绍了UNIX系统中多线程编程的相关知识,包括线程的优缺点、基本线程函数如pthread_create、pthread_join、pthread_self和pthread_detach的用法,以及线程安全函数的概念。文中还通过一个TCP echo Server的实例展示了线程在实际问题中的应用,并强调了参数传递和资源管理的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(本文主要根据 APUE第二版 第11、12章以及UNP (《UNIX network programming--the socket networking API》)第三版第一卷 第26章 学习总结而来,特此声明)


1,简介

    一般的我们在网络服务器端实现多个接入连接的处理时是通过fork()得到多个进程来分别的对付各个接入连接。但是使用fork()产生进程这种方式也是有缺点的,主要的缺点有两点,一是expensive,即由于在产生进程时,是完全的复制了原来进程的各个数据区,包括存储全局变量、全局数据的的heap,自身数据的stack等等,所以需要的存储空间比较多。第二点则是新产生后的进程与原来的父进程之间的沟通比较麻烦,由于是完全的复制,所以新的进程内对许多变量的修改与父进程是独立的(甚至有时候子进程与父进程之间的运行顺序也都是完全随机的,由系统内的调度程序来根据情况与一些策略加以指定),这样当需要父进程能够感知到子进程的修改时,需要他们之间共同的交互数据时,便需要使用信号量等系统级的信息交互机制,显得较为复杂。

   正如我们在OS课程中学习到的基本原理那样,unix中的线程thread则相当于light-process,他的优点就是弥补了以上两点不足。首先,它是与进程共享存储空间的,只有与自身标示、属性相关的一小部分存储需要操作系统为它分配。这样进程的第二个问题也就迎刃而解,子线程完全可以对进程的变量加以修改,而其他该进程内的线程或者该进程自身可以直接获取得到修改后的变量值。当然,由于这种多线程的引入,自然而然的也就有了临界区保护的问题,即如何控制线程的同步性,防止进程之间的读写过程发生混乱,这是需要在使用多线程编程时特别注意的一点。


2,基本的线程相关函数


pthread_create function


#include <pthread.h>

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);

Returns: 0 if OK, positive Exxx value on error


   在我们启动一个程序的时候,系统就自动的设置其为单线程,一个名为initial的线程就默认在执行任务了。而在我们自己的程序中,我们则需要显示的调用该函数生成一个新的线程。

   在该函数中,tid返回的是系统为该线程生成的唯一pid标识号。attr是一些我们在生成该线程时希望进行的一些配置,如对于该线程的优先级、私有stack的大小等,一般情况下,我们直接在此以null为参数,表示使用系统默认的配置。fun则表示希望本线程去执行的函数,这个函数的参数智能通过单参数arg来传入,所以如果敢函数原先有多个参数则需要使用结构体将这些参数封装通过arg这单一参数传入。"The thread starts by calling this function and then terminates either explicitly (by callingpthread_exit) or implicitly (by letting the function return)." 

   返回值为0时表示生产该线程成功,其他正值则表示一些错误信息。

pthread_join function

#include <pthread.h>

int pthread_join (pthread_ttid, void **status);

Returns: 0 if OK, positive Exxx value on error

    在程序中为了进行程序段之间运行顺序的安排,比如指定先后访问某一数据结构的先后顺序,我们可以通过这一函数防止冲突与错误。

   这一函数与进程级别的waitpid函数相似。

   在函数的参数中我们需要指定该线程需要等待的线程,然而遗憾的是,系统中没有给我们提供合适的接口使得能够等待多个甚至所有其他线程结束的直接方法。如果参数汇总state指针non-null则会在该函数返回时,将一些对象指向该参数(与下面pthread_exit函数中的参数相对应)。

pthread_self function

A thread fetches this value for itself usingpthread_self.

#include <pthread.h>

pthread_t pthread_self (void);

Returns: thread ID of calling thread


这一函数与进程级别的getpid相似,主要是返回得到自身的线程ID。

pthread_detach function

A thread is eitherjoinable (the default) ordetached. When a joinable thread terminates, its thread ID and exit status are retained until another thread callspthread_join. But a detached thread is like a daemon process: When it terminates, all its resources are released and we cannot wait for it to terminate. If one thread needs to know when another thread terminates, it is best to leave the thread as joinable.

The pthread_detach function changes the specified thread so that it is detached.

#include <pthread.h>

int pthread_detach (pthread_ttid);

Returns: 0 if OK, positive Exxx value on error


这是一个与线程的退出后相关处理有关的函数,主要是指定该线程是否是可等待的,即该线程在完成操作后是否仍会保留一些自身数据一个阶段,等待等待它的其他那些线程运行到join函数时将这些相关数值传送给它。这个函数通常的作用是让自己直接的结束。不保存。

This function is commonly called by the thread that wants to detach itself, as in

 
pthread_detach (pthread_self());

pthread_exit function

该函数用来在线程内部显式的退出。One way for a thread to terminate is to callpthread_exit.

#include <pthread.h>

void pthread_exit (void *status);

Does not return to caller

If the thread is not detached, its thread ID and exit status are retained for a laterpthread_join by some other thread in the calling process.

The pointerstatus must not point to an object that is local to the calling thread since that object disappears when the thread terminates.

There are two other ways for a thread to terminate:

  • The function that started the thread (the third argument topthread_create) can return. Since this function must be declared as returning avoid pointer, that return value is the exit status of the thread.

  • If themain function of the process returns or if any thread callsexit, the process terminates, including any threads.

3.实例:使用Thread的TCP echo Server 

下面我们通过一个基本简单的例子来具体的说明以上这些函数在具体问题中的应用以及需要注意的一些细节问题。

threads/tcpserv01.c

 1 #include     "unpthread.h"

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

 3 int
 4 main(int argc, char **argv)
 5 {
 6     int     listenfd, connfd;
 7     pthread_t tid;
 8     socklen_t addrlen, len;
 9     struct sockaddr *cliaddr;

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

16     cliaddr = Malloc(addrlen);
17     for (; ; ) {
18         len = addrlen;
19         connfd = Accept(listenfd, cliaddr, &len);
20         Pthread_create(&tid, NULL, &doit, (void *) connfd);
21     }
22 }

23 static void *
24 doit(void *arg)
25 {
26     Pthread_detach(pthread_self());
27     str_echo((int) arg);        /* same function as before */
28     Close((int) arg);           /* done with connected socket */
29     return (NULL);
30 }
  在上述的基本程序段中我们看到的是基本的流程。

其中需要注意的是,在20行中,我们强行的将socket描述符转变为void型指针以满足接口要求。在28行中我们显示的将该socket句柄关闭。这里的关闭是有必要的,因为线程与进程主体共享变量,而主进程中并没有关闭这一描述符,所以线程中再处理完成后需要将其关闭。

如何进行参数的正确传递?   

这里需要注意的是,我们不能通过传递connfd的地址到线程中,因为在主进程中中connfd的数值是在不断更新的,很有可能主进程在更新该值时,子线程的任务还没有完成,这将导致子线程处理的对象可能发生重复、冲突。另一种数值传递的方法时通过malloc和free函数的配合,在住进程中每次生成不同的地址保存connfd,而在子线程中执行完后再将该存储区free掉,这样做是安全的,其主要原因是系统在设计时已经保证了某些系统函数时thread-safe的。

16     cliaddr = Malloc(addrlen);
17     for ( ; ; ) {
18         len = addrlen;
19         iptr = Malloc(sizeof(int));
20         *iptr = Accept(listenfd, cliaddr, &len);
21         Pthread_create(&tid, NULL, &doit, iptr);
22     }
23 }

24 static void *
25 doit(void *arg)
26 {
27     int     connfd;

28     connfd = *((int *) arg);
29     free(arg);

30     Pthread_detach(pthread_self());
31     str_echo(confd);            /* same function as before */
32     Close(confd);               /* done with connected socket */
33     return (NULL);
34 }

Thread-Safe Functions

POSIX.1 requires that all the functions defined by POSIX.1 and by the ANSI C standard be thread-safe, with the exceptions listed inFigure 26.5.

5 Thread-Specific Data


 实战:

下面我们考虑一个多线程进行数据处理的例子:

code:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <stdlib.h>
using namespace std;
struct clove{
int seq;
string name;
     clove(){
     int seq =0;
     string name="";
     }
}; 
vector< struct clove > vcloves;
pthread_rwlock_t v_lock;
int number = 0;


void *add_it(void *arg)
{
  // pthread_detach(pthread_self());
  pthread_rwlock_wrlock(&v_lock);
   struct clove clove_add;
   clove_add.seq = ++number;
   string str("");
  // cin>>str;
   clove_add.name=string((const char *)arg)+str;
   vcloves.push_back(clove_add);
   cout<<"add a item in the vector,and now it has "<<vcloves.size()<<endl;
   pthread_rwlock_unlock(&v_lock);
 return NULL;
}


void *read_it(void *arg)
{
    if(!vcloves.empty())
    {
    pthread_rwlock_rdlock(&v_lock);
   for(auto it = vcloves.begin();it!=vcloves.end();it++)
      cout<<"seq: "<<(*it).seq<<" and name: "<<(*it).name <<endl;
   pthread_rwlock_unlock(&v_lock);
    }
    else
        cout<<"empty"<<endl;
   return NULL;


}
int main(void)
{
    int err = pthread_rwlock_init(&v_lock,NULL);
    if(err!=0)
        return(err);
   while(1)
{
   string str1;
   cin>>str1;
   pthread_t pid1, pid2;
   const char *i=str1.c_str();
   pthread_create(&pid1,NULL,add_it,(void *)i );
//   pthread_join(pid1,NULL);
   pthread_create(&pid2,NULL,read_it,NULL);
  // pthread_join(pid2,NULL);
}
return 0;
}
运行的效果并不如我们所预期的那样(如下图所示),根据显示可以判断出虽然实现了对于global变量的互斥访问,但每一轮中线程的执行顺序是先read_t然后add_it。
 

我们在去掉

//pthread_join(pid1,NULL)

的注释后可以得到我们所希望的结果:


( to be continued...)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值