第十八章 多线程服务器端的实现


线程在windows中的应用比在Liunx平台的应用更广泛。web服务器端协议本身具有的特点,经常需要同时向多个客户端提供服务,开始利用更高效的线程实现WEB服务器端。


理解线程的概念

引入线程的背景

第十章 多进程服务器(下) 第十章 进程与僵尸进程 (上)介绍了多进程服务器端的实现方法。多进程模型与select或epoll相比具有优势,同时也有缺点。创建进程(复制)的工作本身会给操作系统带来相当沉重负担。而且每个进程具有独立的内存空间,所以进程间通信的实现难度也会随之提高。

多进程模型的缺点

创建进程的过程带来一定的开销

为了完成进程间数据交换,需要特殊的IPC技术。

最大的缺点: 每秒少则数十次、多则数千次的上下文切换Context Switching是创建进程时最大的开销。


系统将CPU时间分成多个微小的块后分配给了多个进程。为了分时使用CPU,需要上下文切换。

运行程序前需要将相应进程信息读入内存,如果运行进程A后需要紧接着运行进程B,就应该将进程A相关信息移出内存,并读入进程B的相关信息。但此时进程A的数据将被移动到硬盘,所以上下文切换需要很长时间。即使通过加快速度,也会存在一定的局限。


为保持多进程的优点,同时在一定程序上克服其缺点,引入线程Thread。这是为了将进程的各种劣势降至最低限度(不是直接消除)而设计的一种“轻量级进程”

线程相比于进程优点:

线程的创建和上下文切换比进程的创建和上下文切换更快。

线程间交换数据时无需特殊技术。


线程和进程的差异

每个进程的内存空间都由保存全局变量的数据区、向malloc等函数的动态分配提供空间的堆Heap、函数运行时使用的栈stack构成。每个进程都拥有这种独立空间。

进程A
数据区
堆区域
栈区域

进程B
数据区
堆区域
栈区域

如果以获得多个代码执行流为主要目的,不应出现上述完全分离内存结结构,而只需分离栈区域。这种方式的优势:

上下文切换不需要切换数据区和堆、可以利用数据区和堆交换数据

这就是线程。线程为了保持多条代码执行流而隔开了栈区域,

进程
线程A线程B
线程A栈区域 线程B栈区域
线程共享数据区线程共享数据区
线程共享堆区域线程共享堆区域

线程的内存结构


如上图所示,多个线程将共享数据区和堆。为保持这种结构,线程将在进程内创建并运行。

进程:在操作过程单独执行流的单位。

线程:在进程构成单独执行流的单位。


重载系统内部生成多个执行流,线程就是在同一进程内部创建多条执行流。


线程创建及运行

POSIX是适用于计算机环境的可移植操作系统的接口的简写,是为了提高UNIX系统操作系统间的移植性而制定的API规范。

线程的创建和和执行流程

#include<pthread.h>
int pthread_create(
	pthread_t * restrict thread,const pthread_attr_t * restrict attr,void *(*start_routine) (void *), void * restrict arg
);
成功时返回0,失败时返回其他值。

thread:保存新创建线程ID的变量地址值。线程与进程相同,也需要区分不同线程的ID。

attr:用于传递线程属性的参数,传递NULL时,创建默认的线程。

start_routine: 相当于线程的main函数的、在单独执行流中执行的函数地址值(函数指针)

arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值


P287~P289  通过调用sleep函数向线程提供充足的执行时间。

通过调用sleep函数控制线程的执行相当于预测程序的执行流程,但实际上这是不可能完成的事情。而稍有不慎,很可能干扰程序的正常执行流。所以利用下述的函数控制线程的执行流

#include<pthread.h>
int pthread_join(pthread_t thread,void ** status);
成功时返回0,失败时返回其他值

thread:该参数值ID的线程终止后才会从该函数返回

status:保存线程的main函数返回值的指针变量地址值

调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。而且可以得到线程的main函数返回值,所以该函数比较有用。


P290~P291 调用pthread_join函数的流程图


可在临时区调用的函数

关于线程的运行需要靠“多个线程同时调用函数时(执行时)可能产生问题”。这类函数内部存在临界区Critical Section,也就是说,多个线程同时执行这部分代码时,可能引起问题
根据临界区是否引起问题,函数分为2类:

线程安全函数 Thread-safe function(被多个线程同时调用也不会引发问题)

非线程安全函数 Thread-unsafe function(被同时调用时引发问题)

线程安全函数中同样存在临界区,只是在线程安全函数中,同时被多个线程调用时可通过一些措施避免问题、

线程安全函数的名称后缀通常为_r。可以通过“声明头文件前定义_REENTRANT宏”,将自动把函数调用加上后缀_r的函数调用。


工作Worker线程模型

计算1~10的和,不在main函数中进行累加运算。而是创建2个线程,其中一个线程计算1~5的和,另一个线程计算6~10的和,main函数只负责输出运算结果。这种方式的编程模型称为工作模型worker thread、两个线程将成为main线程管理的工作worker。
P292~P294的代码示例需要注意 2个线程直接访问全局变量sum。

线程存在的问题和临界区

多个线程访问同一变量是问题

2个线程直接访问全局变量sum,此处的访问是指 值的更改。虽然访问的对象是全局变量,但这并非全局变量引发的问题。任何内存空间-只要被同时访问,都可能发生问题。

线程访问变量num时应该阻止其他线程访问,直到线程完成运算。这就是同步,Synchronization。


临界区位置

函数内同时运行多个线程时引起问题的多条语句构成的代码块。

临界区通常位于由线程运行的函数内部。

P298~P299综上所述,可得 临界区并非num本身,而是访问num的2条语句。


线程同步

同步的两面性

线程同步用于解决线程访问顺序引发的问题。需要同步的两方面情况:

同时访问同一内存空间时发生的情况

需要知道访问同一内存空间的线程执行顺序的情况 。(这是 控制线程执行顺序的相关内容,设AB两个线程,A负责向指定内存空间写入数据,B负责取走数据。这种情况下,A首先应该访问阅读的内存空间并保存数据。万一B先访问并取走数据,将导致错误结果。)


互斥量 Mutual Exclusion

表示不允许多个线程同时访问。主要用于解决线程同步访问的问题。

互斥量的创建及销毁函数

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex)
成功时返回0,失败时返回其他值

mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要消耗的互斥量地址值

attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL


利用互斥量锁住或释放临界区时使用的函数

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex)
成功时返回0,失败时返回其他值

调用该函数时,发现有其他线程已进入临界区,则 pthread_mutex_lock函数不会返回,直到里面的线程调用 pthread_mutex_unlock函数退出临界区为止。也就是说,其他线程让出临界区前,当前线程将一直处于阻塞状态。


利用lock和unlock函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问。

线程退出临界区时,若忘记调用pthread_mutex_unlock函数,其他为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。称之为死锁、(Dead-LOCK)

信号量

利用二进制信号量(0或1)完成控制线程顺序为中心的同步方法。

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);//操作系统将创建信号量对象,对象中记录着信号量值整数
int sem_destroy(sem_t *sem);
成功时返回0,失败时返回其他值

sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值

pshared:传递其他值时,创建可以由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。需要完成同一进程内的线程同步,故传递0

value:指定新创建的信号量初始值


信号量中相当于lock和unlock的函数

#include<semaphore.h>
int sem_post(sem_t *sem);//信号量值整数在调用此函数时增1
int sem_wait(sem_t * sem);//调用此函数时减1
成功时返回0,失败时返回其他值

sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1

注意:信号量值不能小于0,在信号量为0情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。



线程的销毁和多线程并发服务器端的实现

销毁线程的3种方法

Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下2种方法之一加以明确。否则由线程创建的内存空间将一直存在。

调用pthread_join函数(调用该函数时,会等待线程终止,还会引导线程销毁。该函数有问题,线程终止前,调用该函数的线程将进入阻塞状态。通常用detach函数引导线程销毁)

调用pthread_detach函数

#include<pthread.h>
int pthread_detach(pthread_t thread);
成功时返回0,失败时返回其他值

thread:终止的同时需要销毁的线程ID

调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间调用该函数后不能再针对相应线程调用pthread_join函数。

网络编程(41)—— Linux线程销毁的两种方法



















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值