美文网首页
Chapter1: alarm 程序 3 种 版本 - Prog

Chapter1: alarm 程序 3 种 版本 - Prog

作者: my_passion | 来源:发表于2022-05-17 21:05 被阅读0次
    << Programming with POSIX threads >> 
    
    作者 R. Butenhof
    
    https://gitee.com/msntec/posix-threadBlocks-programming
    
    编译
        安装 CMake
        git clone https://github.com/Veinin/programming-with-POSIX-threadBlocks-tutorials.git
        cd programming-with-POSIX-threadBlocks-tutorials
        ./build.sh
    
    运行
        第1个程序
    
        $ ./bin/barrier_main
        00: (10)0000045001 0000045002 0000045003 0000045004 0000045005 0000045006
        ...
    

    chapter1 概述

    1.1 定义 + 术语
    
    (1) 异步
        
        操作(event) 间 关系: 独立
        
            操作 独立 发生/前进: 不必 一个等另一个完成后才能开始
                    
                 除非 被 强制依赖性
        
    (2) 并发 
        
        多个 序列/操作 前进(执行) 的方式: 分时 交错
            
            表面上 同时执行
            实际上 `同时只有 1 个执行` 
        
    (3) 并行  
        
        并发 序列 同时前进 
        
        并发/性 序列 可/只能 在 单/多 处理器 系统上出现 
    
    (4) 单处理器 与 多处理器
    
        多处理器    
            共享 指令集 和 物理内存
    
    (5) 线程安全
    
            不要求 高效性
           
        大多 现有函数 -> 改为 线程安全的版本 的方法 
            
            用 Pthreads 的 mutexes / cv / thread private data 
            
            [1] 函数 串行化: 函数 进入时 lock, 退出时 unlock
                
                用于: 不要求 持续的 context 的 func
                    
            [2] 临界区 串行化 + 非临界区 可并行
            
                函数 -> 分解为 -> 各 临界区
                
            [3] protect `临界 data` 而不是 `临界 code`: 重新设计
    
                1] 不同时使用 临界 data 时
                        
                    可完全 `并行 执行 code`
                                
                2] 同时使用 临界 data 时
                                
                    仍可   `并发 访问 临界 data` 
    
            // 例 
            putchar 函数: 写 字符 到 I/O buffer -> 线程安全的 版本
            
                1]  protect `临界 code`: 关联 mutex 与 func
                
                    lock "putchar mutex" 
                    write 字符
                    unlock "putchar mutex"
    
                2]  protect `临界 data`: 关联 mutex 与 stream 
                    
                                    
                比较
                    2 个线程 putchar 到 不同 streams
                        1]只能有1个线程 / 2] 2个线程 
                            同时执行 putchar 
                            
    (6) 可重入
    
        "高效的 线程安全"
                
        可重入 code 应 避免 依赖于
            
            1] static data
            
            2] 线程间 同步 
            
        vs. mutex 和 线程特定数据
                
            通常需要 改变 函数 interface
                    
        避免 函数内部同步 的 方法 
            
            [1] 函数 保存 状态到 "context 结构" -> 让 caller 控制 
                
                caller 负责 data 的 同步 
                
                    UNIX readdir()
                        
                        顺序地 返回每个 目录入口 
                        进入 时 lock mutex 
                        返回 前 unlock 
            
            [2] caller 分配 维持函数 的 `context 结构` 
                
                Pthreads readdir_r() 
                    
                1] 表面上, 只是将 func 的 责任移交给 caller 
                        
                2] 实际上, 只有 caller 知道 如何使用 the data (context)
                    
                    1> 若 只有1个线程 用该 contexct, 
                        
                        则 不必同步 
                    
                    2> 有 多个线程 共享数据, 
                    
                        用于该 `context` 的 mutex 还可以用于 `其他 data`
    
    (7) 并发 控制函数
        
        并发系统 
    
        [1] 要提供的 必要函数, 创建 并发执行 contexts 
        
        [2] 控制 这些函数 如何 运行
        
        3 种 便捷方法(facilities)
            
            [1] `执行 context` 是 并发实体 的 state
                    
                并发系统 要能对 多个 contexts 
                    
                    创建 / 删除 / 独立 `维持` 其 state 
                    
                save context 的 state 
                dispatch(分发) 到 另一 context 
                        
                外部事件 -> 中断 -> 回到最后的执行处: 相同的 寄存器 内容 
                    
            [2] schedule 
                
                决定 任意给定时刻, 哪个 context 被 执行 
                
            [3] synchronize 
                
                `并发 执行 contexts` 时, 协调 `shared resources`
    
        TABLE 1.1 Execution contexts, schedulers, and synchronization
        ——————————————————————————————————————————————————————————————————————————————————————————————————————————      
                                Execution context   Scheduling                      Synchronization
        ——————————————————————————————————————————————————————————————————————————————————————————————————————————
        Real traffic            automobile          traffic lights and signs    turn signals and brake lights
        ——————————————————————————————————————————————————————————————————————————————————————————————————————————
        UNIX(before threadBlocks)   process             priority (nice)             wait and pipes
        ——————————————————————————————————————————————————————————————————————————————————————————————————————————
        Pthreads                thread              policy, priority            condition variables and mutexes
        ——————————————————————————————————————————————————————————————————————————————————————————————————————————
    
        调度 
            run until block
                自动让出 cpu
            
            round-robin
                时间片 => 周期性 让出 
                
        同步 
            
            4种机制 
                
                mutex
                cv 
                信号量 
                事件
                 
                消息传递机制 
                    
                    UNIX pipes 
                    sockets 
                    POSIX 消息队列 
    
    1.2 异步编程 是 直观的
        
        UNIX shells
            shell 是 异步编程
        
    (1) UNIX 是 异步的
    
        UNIX 系统中,`进程 间 异步执行`
        
        向 shell 键入命令时, 
            实际上启动了 `1个 独立的程序` —— 若您在 `后台运行` 该程序, 它会
                与 shell 异步运行
                
    1.4 异步编程 例子 
    
        程序在循环中 提示 `输入行`, 直到 在 stdin 上 收到 错误或结束
            每一行中, 
                第1个 非空白 被解释为 等待的秒数
                其余部分(最多 64 个字符)是一条消息, 等待完成时将打印
            
    (1) 基线: 同步 (sleep) 版本 
    
        循环 
            从 stdin 读 1行
            解析: 为 要等待的秒数 + 要打印的 msg
            等待 + 打印
            
        问题
            1次 只能有1个 alarm 请求 被激活
    
    (2) 多进程 版本: 异步
        
        1个请求 1个子进程 去 处理
        
        父进程 waitpid 回收 terminated 子进程
    
        1) 思路
    
            [1] `为 每个 command, fork 1个 子进程`: copy 主进程 地址空间 -> execute 
    
            [2] 可 随时输入 command, 各 commands 独立进行
    
            [3] 与 同步版本 区别
                
                    不直接调 sleep
                
                1] 用 fork 创建 子进程
                
                2] 子进程 异步调 sleep
                
                3] 父进程 继续
    
        2) `回收` 任何 `已终止的子进程`
                
            必要 
                否则, 系统将保存 这些子进程, 直到程序终止
            
            方法
            
                waitpid()
                    
                与 wait() 区别  
                    可指定 要清理的 pid 进程
                    `可 不阻塞`
                        允许 caller 指定 WNOHANG
    
                return 
    
                    1]  0 (进程 ID 0)
                    
                    2] -1 : 出错
                    
                    3] 非 0 && 非 -1 
                        `还有 需要回收的 已终止子进程`, 
                            立即回收, 返回 `非 0` 
                    
    (3) 多线程 版本: 异步
    
        1个 请求, 1 个线程
    
        1) 与 多进程 区别
        
            1]  `线程` 而非进程 
            2]  `堆内存` 而非 栈内存 
                    
        2) 必要时, Pthreads 会 持有 线程资源, 以便 
            
            另一线程 可 
                1] 确定 当前线程 已退出 
                2] 获取最终结果
    
    (4) 总结 
    
        1) 地址空间
    
            进程版本 
                
                各 进程 有 `独立的地址空间`, 从 主程序 copy 而来
    
                    => 进程要处理的 data 可放 stack/局部变量
                
                        父子进程 用 2套独立数据 => 相互不影响
                    
            线程版本
                各 线程 share 所属进程的 地址空间
                    
                    => 每个 新线程 要处理的 data 可用 malloc + 传 pData
    
        2)
            进程版本
                
                主程序要通过调 waitpid 或 wait 等,
                
                    来告诉 内核 释放 子进程 资源
                    
                    recycle 所有已完成的子进程
    
            线程版本
            
                除非 需要线程的 返回值, 否则不需要等待线程
                
                    每个线程会 自行 detach, 以便 线程 terminate 时, 线程持有的 resource 立即返回
    
        更复杂的 线程版本 
            
            两个线程
            
                线程1: 读 用户输入
                线程2: 等待下一个到期 警报
                
        // ====== 1. alarm.c
        #include "errors.h" // 包含 <unistd.h> and <stdio.h> 错误报告宏
    
        int main(void) 
        {
            int seconds;
            char line[128];   // 存 从 stdin 中 读取的 行
            char message[64]; // 存 解析出的 msg
    
            while(1) 
            {
                // (1) fgets 从 stdin 读取1行 放到 char 数组 line, error 或 eof 时 返回 NULL
                fgets(line, sizeof(line), stdin);
    
                if (strlen(line) == 0)
                    continue;
    
                // (2) sscanf 解析 fgets 读取的 行: 分离 由 空格(blank) 分隔的 
                //      1] 要 wait 的 秒数  2] 要 print 的 msg (最多 64 个字符, 不含 '\n' )
                sscanf(line, "%d %64[^\n]", &seconds, message);
                
                // (3)
                sleep(seconds); 
                printf("(%d) %s\n", seconds, message);
            }
        }
        
        // ====== 2. alarm_fork
        #include <sys/types.h>
        #include <wait.h>
        #include "errors.h"
    
        int main(void) 
        {
            pid_t pid;
            int seconds;
            char line[128];
            char message[64];
    
            while(1) 
            {
                fgets(line, sizeof(line), stdin);
    
                if (strlen(line) <= 1)
                    continue;
    
                if (sscanf(line, "%d %64[^\n]", &seconds, message) < 2) 
                {
                    fprintf(stderr, "Bad command\n");
                } 
                else 
                {
                    // (1) fork
                    pid = fork();
                    
                    // (2) -1: 出错
                    if (pid == -1)
                        errno_abort("Fork");
                    
                    // (3) 0: 子进程
                    if ( pid == (pid_t)0 ) 
                    {
                        sleep(seconds);
                        printf("\n");
                        
                        // 取 本/子 进程 ID 
                        pid = getpid();
                        printf("Child process pid is (%d)\n", pid);
                        printf("(%d) %s\n", seconds, message);
                        
                        // Note
                        exit(0);
                    } 
                    else // (4) >0 (子进程 ID): 父进程
                    {
                        // 1) 取 本/父 进程 pid
                        pid = getpid();
                        printf("Parent process pid is (%d)\n", pid);
                        
                        // 2) 回收 `已终止的子进程`
                        do 
                        {
                            pid = waitpid((pid_t)-1, NULL, WEXITED);
                            
                            if (pid == (pid_t) -1)
                                errno_abort("Wait for child");
                                
                        } while( pid != (pid_t)0 );
                    }
                }
            }
        }
            
        // ====== 3. alarm_thread.c
        #include "errors.h"
        #include <pthread.h>
    
        // 控制包 / control packet 
        typedef struct AlarmTag 
        {
            int  seconds;
            char message[64];
        } Alarm;
    
        void *alarm_thread(void *arg) 
        {
            // 子线程 `自行 分离`
            // (3) pthread_detach: 允许 Pthread 线程终止后 立即回收 线程的资源
            // (4) pthread_self: 返回 调用线程的标识符
            pthread_detach( pthread_self() ); 
        
            Alarm *alarm = (Alarm*)arg;
            
            // (5)
            sleep(alarm->seconds);
            
            printf("(%d) %s\n", alarm->seconds, alarm->message);
            
            // (1-2) 子线程 内 free 主线程 malloc 的 内存
            free(alarm);
            
            return NULL;
        }
    
        int main(void) 
        {
            int seconds;
            char line[128];
            Alarm *alarm;
            
            // (0) 线程对象
            pthread_t thread;
    
            while(1) 
            {
                fgets(line, sizeof(line), stdin);
    
                if (strlen(line) <= 1)
                    continue;
    
                // (1) malloc + free 在 异常 / 子线程 中都要有
                alarm = (Alarm*)malloc( sizeof(Alarm) );
    
                sscanf(line, "%d %64[^\n]", &alarm->seconds, alarm->message);
    
                pthread_create(&thread, NULL, alarm_thread, alarm);
    
            }
        }
                    
    1.5 线程 收益 
        
        多线程编程模型 优点
    
            (1) 在 CPU 上 并行性
            
            (2) 更有效地利用程序的自然 并发性
            
                程序可 `等待 慢速 I/O 操作完成 时, 执行 计算`
                    
            (3) 模块化编程模型
                    
                    清晰表达 code 中 独立 "事件" 间 关系
        
    1.6 线程 代价 
    
    (1) 计算开销
    (2) 编程 更严格
    (3) 难调试 
        
    1.7 用 线程 还是 不用 ?
    
    (1) 不该用线程的 case 
        
        问题 "本质上 非并发"
        
            线程版本 
                减慢程序 + 复杂化
                
    (2) 该用线程 的 case
    
        [1] 大量计算 可 并行/分解 为 多个线程, 且 想要运行到 多 CPU 上 
    
        [2] 大量 I/O 
            
            多线程 可同时等待 不同的 I/O 请求
        
            分布式 server 适合用 多线程 
                
                1] 要 响应多个 client 
                
                2] 要为 较慢的网络连接 上 `不请自来的 I/O` 做好准备
                
                
    1.8 POSIX 线程 概念 
    
    (1) 架构 概述
    
        线程系统 3个基本方面
            context 
            schedule 
            synchronize
        
        1) create 执行上下文(线程) 
                
            pthread_create
        
        2) Pthreads 指定 `调度 参数` 的时机 
        
            [1] 创建线程 
            
            [2] 线程运行 时
    
        3) 线程 `终止` 时机 
            
            [1] 调 pthread_exit 时
            
            [2] 从 线程启动函数 返回 时
    
        4) Pthreads 同步模型
        
            [1] 用 mutex 保护
                
                mutex 允许线程
                    用 shared data 时, lock 它
                        以免其他线程 干扰
                
            [2] 用 cv 通信
            
                cv 允许线程
                    wait 共享数据 `到达 某种 期望的状态` ( "队列不为空" / "资源可用" )
            
            [3] 信号量 / 管道 / 消息队列
    
    (2) 类型 和 接口 
        
        TABLE 1.2 POSIX threadBlocks types
        ————————————————————————————————————————————————————————
        Type                Section Description
        
        pthread_t           2 i     thread identifier
        
        pthread_mutex_t     3.2     mutex
        pthread_cond_t      3.3     cv
        
        pthread_key_t       5.4     线程特定数据 的 “access key” 
        
        pthread_attr_t      5.2.3   thread attributes object
        pthread mutexattr_t 5.2.1   mutex attributes object
        pthread_condattr_t  5.2.2   cv attributes object
        
        pthread_once_t      5.1     "one time initialization" control context
        ————————————————————————————————————————————————————————
        
    (3) 报告错误
        
        Pthreads 函数 出错时 不置 errno -> 用 新方法 报告错误
        
        // errors.h
        #ifndef ERRORS_H
        #define ERRORS_H
    
        #include <unistd.h>
        #include <errno.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
    
        #define err_abort(code, text) \
            do {\
                fprintf(stderr, "%s at \"%s\":%d: %s\n",\
                    text, __FILE__, __LINE__, strerror(code));\
                abort();\
            } while(0)
    
        #define errno_abort(text) \
            do {\
                fprintf(stderr, "%s at \"%s\":%d: %s\n",\
                    text, __FILE__, __LINE__, strerror(errno));\
                abort();\
            } while(0)
    
        #endif // ERRORS_H
        
        // thread_error.c
        #include <pthread.h>
        #include <stdio.h>
        #include <string.h>
        #include <errno.h>
    
        int main()
        {
            pthread_t thread;
            pthread_join(thread, NULL);
        }
    

    相关文章

      网友评论

          本文标题:Chapter1: alarm 程序 3 种 版本 - Prog

          本文链接:https://www.haomeiwen.com/subject/uxdiurtx.html