Linux网络编程之POSIX线程(一)-pthread_create…

学习了进程间通信的介绍之后,下面要学习线程的相关知识。线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。下面主要内容有:线程基本概念,POSIX线程等一系列函数。

1.线程相关概念
(1)什么是线程

1)在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。

2)一切进程至少都有一个执行线程。

(2)进程与线程

1)进程是资源竞争的基本单位
2)线程是程序执行的最小单位

3)线程共享进程数据,但也拥有自己的一部分数据

  • 线程ID
  • 一组寄存器
  • errno
  • 信号状态
  • 优先级

4)fork和创建新线程的区别

a)当一个进程执行一个fork调用的时候,会创建出进程的一个新拷贝,新进程将拥有它自己的变量和它自己的PID。当这个新进程的运行时间是独立的,它在执行时几乎完全独立于创建它的进程。

b)在进程里面创建一个新线程的时候,新的执行线程会拥有自己的堆栈(因此也就有自己的局部变量),但要与它的创建者共享全局变量、文件描述符、信号处理器和当前的工作目录状态。

(3)线程优缺点

优点:

1)创建一个新线程的代价要比创建一个新进程小得多
2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3)线程占用的资源要比进程少很多
4)能充分利用多处理器的可并行数量
5)在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6)计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7)I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:

1)性能损失:一个很少被外部时间阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

2)健壮性降低:编程线程程序需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

3)缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

4)编写难度提高:编写与调试一个多线程程序比单线程程序难的多。

(4)线程模型

1)讲述线程模型之前,引入一个概念——线程调度竞争范围:

  • 操作系统提供了各种模型,用来调度应用程序创建的线程。这些模型之间的主要不同是:在竞争资源(特别是CPU时间)时,线程调度竞争范围(thread-scheduling contention scope)不一样:
  • 进程竞争范围(process contention scope):各个线程在同一进程竞争“被调度CPU时间”(但不直接和其他进程中的线程竞争)。
  • 系统竞争范围(system contention scope):线程直接和“系统范围”内的其他线程竞争。

2)线程模型

  • N:1用户线程模型
  • 1:1核心线程模型
  • N:M混合线程模型

N:1用户线程模型N:1用户线程模型

a)“线程实现”建立在“进程控制”机制之上,由用户空间的程序库来管理。OS内核完全不知道线程信息。这些线程称为用户空间线程。
b)这些线程都工作在“进程竞争范围”。

c)在N:1线程模型中,内核不干涉线程的任何生命活动,也不干涉同一进程中的线程环境切换。
d)在N:1线程模型中,一个进程中的多个线程只能调度到一个CPU,这种约束限制了可用的并行总量
e)第二个缺点是如果某个线程执行了一个“阻塞式”操作(如read),那么,进程中的所有线程都会阻塞,直至那个操作结束。为此,一些线程的实现是为这些阻塞式函数提供包装器,用非阻塞版本替换这些系统调用,以消除这种限制。

1:1核心线程O型1:1线程模型

a)在1:1核心线程模型中,应用程序创建的每一个线程都由一个核心线程直接管理
b)OS内核将每一个核心线程都调到系统CPU上,因此,所有线程都工作在“系统竞争范围”。
c)这种线程的创建于调度由内核完成,因为这种线程的系统开销比较大(但一般来说,比进程开销小)。

N:M混合线程模型N:M混合线程模型

N:M混合线程模型提供了两级控制,将用户线程映射为系统的可调度体以实现并行,这个可调度体称为轻量级进程(LWP,lightweight process),LWP再一一映射到核心线程。

2.POSIX线程

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都以“pthread_”开头的;要使用这些函数库,要通过引入头文件<pthread.h>;链接这些线程函数库时要使用编译器命令的“-lpthread”选项。

POSIX线程库相关函数:

(1)pthread_create函数

  • 功能:创建一个新的线程
  • 原型:
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • 参数:
    • thread:返回线程ID
    • attr:设置线程的属性,attr为NULL表示使用默认属性
    • start_routine:是个函数地址,线程启动后要执行的函数
    • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码

1)此处,不同于前面介绍的一些函数错误时返回-1,这里失败了是返回错误码:

a)传统的一些函数是,成功返回0;失败返回-1,并且对全局变量errno赋值以指示错误。
b)pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。(观察下面示例错误处理的操作即可明白二者的不同)
c)pthreads同样也提供了线程的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。

2)编写一个程序,来创建一个线程:

a)定义一个线程id变量,然后调用pthread_create函数:
pthread_t tid;    //创建线程成功后,线程ID由此参数返回
int ret;
ret = pthread_create(&tid, NULL, thread_routine, NULL);//其中第三个参数是一个函数指针,是线程的入口函数,形式如下:
void *thread_routine(void *arg)
{
return 0;
}

b)在初始线程中打印输出A,在新创建的线程中打印输出B(即在入口函数中打印B)。
c)编译运行后会发现有一个问题:初始线程(暂且称其为主线程,它们并不是父子关系)和新创建的线程内部各自打印输出20个字母A和B,但是呢,当主线程内部的A打印完之后,就会转入return 0;结束程序,这个时候新创建的线程也会被迫结束,即使是该线程的B没有完全输出!

3)针对上述问题,有两个解决方案:其一,在主线程将要结束的时候sleep()一段时间供新创建的线程运行完;其二,使用pthread_join函数(类似于waitpid等待子进程结束):
pthread_join(tid, NULL);

(2)pthread_join函数

  • 功能:等待线程结束
  • 原型:
    int pthread_join(pthread_t thread, void **value_ptr);
  • 参数:
    • thread:线程ID
    • value_ptr:它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回0;失败返回错误码

1)关于僵线程:子进程先结束了而父进程还没有结束,这个时候子进程会保留一个状态直到父进程调用waitpid之后,僵进程状态才会解除。

2)同样的线程也有僵线程的概念:新创建的线程结束了而主线程没有调用pthread_join,那么该新创建的线程就会处于僵线程的状态。

(3)pthread_exit函数

  • 功能:线程终止
  • 原型:void pthread_exit(void *value_ptr);
  • 参数
    • value_ptr:value_ptr不要指向一个局部变量
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

1)使用该函数对我们之前打印AB的函数进行改造:在入口函数中,即新创建的线程中输入如下代码:
if (i == 3)
pthread_exit(“ABC”);            //”ABC”为一个退出时返回的值,若不关心可以置为NULL。若关心,如下示例,我们可以将其传给pthread_join函数的第二个参数中(类似于进程中,exit退出(或return退出)时,可以传给waitpid函数中的参数一样),并且可以打印输出检验:
void *value;
pthread_join(tid, &value);
printf(“return msg = %s\n”, (char*)value);

2)多次修改之后的完整程序如下:

(4)pthread_self函数

  • 功能:返回线程ID
  • 原型:pthread_t pthread_self(void);
  • 返回值:成功返回0

(5)pthread_cancel函数

  • 功能:取消一个执行中的线程
  • 原型:int pthread_cancel(pthread_t thread);
  • 参数:
    • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

某个线程调用该函数可以杀死另一个线程,类似于进程中的kill函数。(属于“他杀”,而调用pthread_exit或在入口函数中return返回,属于“自杀”)

(6)pthread_detach函数

  • 功能:将一个线程分离
  • 原型:int pthread_detach(pthread_t thread);
  • 参数:
    • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

1)接着前面pthread_join关于僵线程的问题讨论,有的时候对于某些线程,主线程不会调用pthread_join函数来避免僵线程。

2)这个时候就需要将线程的属性设置为脱离的,如果我们在创建线程的时候没有设置它的属性为脱离的,可以调用函数pthread_detach来脱离一个线程。对于一个脱离的线程就不会产生僵线程。

3.用线程实现回射客户/服务器

(1)使用线程代替socket编程章节中使用进程方式实现的回射客户/服务器,整体框架不变,对服务器端而言:先创建套接字–>初始化服务器端地址–>绑定地址结构–>监听–>初始化对等方地址–>连接–>通信。

(2)这里要做的修改,是在连接后的通信阶段:创建一个新线程来处理echo_srv(conn)通信模块:

pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thread_routine, (void*)conn);

(3)入口函数thread_routine:

void * thread_routine(void *arg)
{
int conn = (int)arg;
echo_srv(conn);
}

(4)修改之后的程序就不需要在子进程中close(listenfd);关闭监听套接字和在父进程中close(conn);关闭已连接套接字了。因为这里是以线程的实现,只有一个进程。

使用线程修改后的echosrv.c

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注