《TCP/IP网络编程》第18章 多线程服务器端的实现

本章所有示例代码>>gtihub

18.1 理解线程的概念

1.       引入线程的背景

    多进程模型的缺点:

  • 创建进程的过程会带来一定的开销;
  • 为了完成进程间数据交换,需要特殊的IPC技术;

  • “每秒少则数十次、多则数千次的‘上下文切换’(ContextSwitching)是创建进程时最大的开销。”

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

    上下文切换:运行程序前需要将相应进程信息读入内存,如果运行进程A后需要紧接着运行进程B,就应该将进程A相关信息移出内存(移动到硬盘),并读入进程B相关信息。

    线程相比于进程具有如下优点:

  • 线程的创建和上下文切换比进程的创建和上下文切换更快;
  • 线程间交换数据时无需特殊技术;

2.       线程和进程的差异

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

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


    进程:在操作系统构成单独执行流的单位;

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

18.2 线程创建及运行

1.       线程的创建和执行流程

    线程具有单独的执行流,因此需要单独定义线程的main函数,还需要请求操作系统在单独的执行流中执行该函数。

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

    -thread:        保存新创建线程ID的变量地址值。区分不同线程的ID;

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

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

    -arg:             传递调用函数时包含传递参数信息的变量地址值;

    restrict关键字、函数指针。

    线程执行流程:


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

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

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

    简言之,调用该函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止;

2.       可在临界区内调用的函数

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

    根据临界区是否引起问题,函数可分为以下2类:

  • 线程安全函数(Thread-safefunction)
  • 非线程安全函数(Thread-safefunction)

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

    幸运的是,大多数标准函数都是线程安全的函数。相关平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全函数。如gethostbyname是非线程安全的函数,gethostbyname_r是线程安全的函数。(Linux平台下线程安全函数的名称后缀通常为_r)

    “声明头文件前定义_REENTRANT宏。”(也可以在编译时通过添加-D_REENTRANT选项定义宏)

3.       工作(Worker)线程模型

    计算1到10的和。其中一个线程计算1到5的和,另一个线程计算6到10的和,main函数只负责输出运算结果。这种编程模型称为“工作线程(Workerthread)模型”。

18.3 线程存在的问题和临界区

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

    任何内存空间——只要被同时访问——都可能发生问题。

    假设2个线程要执行将变量值逐次加1的工作。

    需要注意值的增加方式,值的增加需要CPU运算完成,变量num中的值不会自动增加。线程1首先读该变量的值并将其传递到CPU,获得加1之后的结果100,最后再把结构写回变量num,这样num中就保存100。接着线程2再做处理可得正确结果。但是,线程1完全增加num值之前,线程2完全可能通过切换得到CPU资源。因此,线程访问变量num时,应该阻止其他线程访问,直到线程1完成运算,这就是同步(Synchronization)。

2.       临界区位置

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

18.4 线程同步

1.       同步的两面性

    线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两方面入手:

  • 同时访问同一内存空间时发生的情况;
  • 需要指定访问同一内存空间的线程执行顺序的情况;

    假设A、B两个线程,线程A负责向指定内存空间写入(保存)数据,线程B负责取走该数据。(应控制执行顺序)

2.       互斥量(Mutual  Exclusion,Mutex)

    Mutual Exclusion不允许多个线程同时访问。互斥量主要用于解决线程同步互斥问题。

    互斥量的创建及销毁函数:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t*mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destory(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_lock函数不会返回,直到里面的线程调用pthread_mutex_unlock函数退出临界区为止(当前线程将一直处于阻塞状态)。

    线程进入临界区时,如果忘了调用pthread_mutex_unlock函数,那么其他为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况称为“死锁”(Dead-lock)。

    临界区划分较大——“最大限度减少互斥量lockunlock函数的调用次数。”,因为互斥量lockunlock函数调用过程时间花费较大。

3.       信号量(Semaphore)

    信号量创建及销毁方法:

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared,usigned int value);
int sem_destory(sem_t *sem);
//成功时返回0,失败时返回其他值

    -sem:         信号量变量地址值;

    -pshared:   传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量(为完成同

                         一进程内的线程同步,故传递0);

    -value:       制定新创建的信号量初始值;

#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
// 成功时返回0,失败时返回其他值

    调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”(SemaphoreValue)整数,该值在调用sem_post函数时增1,在调用sem_wait函数时减1。

    但信号量的值不能小于0。因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。此时如果有其他线程调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将信号量重新减为0并跳出阻塞状态。

    假设信号量初始值为1,完成临界区的同步操作:

    sem_wait(&sem);

    // 临界区

    sem_post(&sem);

    上述代码结构中,调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区。信号量的值在0和1之间跳转,因此,具有这种特性的机制称为“二进制信号量”。

    信号量用于控制各线程按既定顺序访问资源。

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

1.       线程销毁的3种方法

  • 调用pthread_join函数;
  • 调用pthread_detach函数;

    调用该函数时,不仅会等待线程终止,还会引导线程销毁,但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。通常通过如下函数调用引导线程销毁:

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

    可以通过该函数引导销毁线程创建的内存空间。



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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值