当前位置:首页 > 随笔 > 正文内容

线程控制与同步机制

访客 随笔 2026年9月7日 6

12.1 线程配置

  1. 在创建线程时,如果不需要跟踪其终止状态,可以使用pthread_attr_setdetachstate方法。
  2. 使用pthread_attr_init进行初始化,而pthread_attr_destroy用于清理资源。
  3. 每个线程共享相同的虚拟地址空间。当线程栈空间不足时,可使用pthread_attr_setstack函数调整新线程的栈位置。
  4. 若希望更改默认栈大小但不想自行处理栈分配,可以使用pthread_attr_setstacksize。
  5. guardsize参数定义了线程栈末尾后用于防止溢出的额外内存大小。

12.2 同步设置

12.2.1 互斥锁配置

包括进程共享、健壮性和类型属性。

  1. 进程共享属性
PTHREAD_PROCESS_PRIVATE  多个线程可访问同一同步对象。
PTHREAD_PROCESS_SHARED  多个进程访问共享数据时需要同步

  1. 健壮属性涉及多个进程间的互斥锁。
PTHREAD_MUTEX_STALLED   当持有锁的进程结束时无需特别处理
PTHREAD_MUTEX_ROBUST  返回EOWNERDEAD

  1. 类型属性:
PTHREAD_MUTEX_NORMAL     重复加锁会导致死锁
PTHREAD_MUTEX_ERRORCHECK  重复加锁返回错误
PTHREAD_MUTEX_RECURSIVE  可重入锁
PTHREAD_MUTEX_DEFAULT   未定义

12.2.2 读写锁配置

仅支持进程共享属性,与互斥锁相同。

12.2.3 条件变量配置

包含进程共享属性和时钟属性。

12.2.4 屏障配置

目前只定义了进程共享属性,控制屏障是否被多进程中的线程使用。

12.3 重入性

1. 线程安全:

若一个函数能被多个线程同时调用而不会导致问题,则称为线程安全。

实现方式包括:

  • 使用互斥量保护全局变量
  • 避免使用全局变量

2. 可重入:

若一个函数能被多次安全调用,称为可重入。

分为两种情况:

  • 多个线程同时调用该函数,称为对多线程可重入。
  • 同一线程因信号处理程序多次调用该函数,称为对信号处理程序可重入。

实现方式:

  • 不使用全局变量,每个调用者传入自己的缓存区。

3. 关系:

若函数对多线程可重入,则它也是线程安全的。但这不意味着对信号处理程序也是可重入的。

若函数对异步信号处理程序是安全的,则称其为异步信号安全。

需使用递归互斥量确保线程安全,并防止来自信号处理程序的死锁。

示例代码:

#include <pthread.h>
#include <string.h>
#include <errno.h>
#include "apue.h"

extern char **environ;

pthread_mutex_t env_mutex;

static pthread_once_t init_once = PTHREAD_ONCE_INIT;

static void thread_init(void) {
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //可重入锁,防止死锁
    pthread_mutex_init(&env_mutex, &attr);
    pthread_mutexattr_destroy(&attr);
}

int getenv_r(const char *name, char *buf, int buflen) {
    int i, len, olen;

    pthread_once(&init_once, thread_init); //保证初始化函数只执行一次
    len = strlen(name);
    pthread_mutex_lock(&env_mutex);
    for (i = 0;  environ[i] != NULL; i++) {
        if ((strncmp(name, environ[i], len) == 0) &&
        (environ[i][len] == '=')) {
            olen = strlen(&environ[i][len+1]); //数组名等同于指着,下标取值等于取内容
            if (olen >= buflen) {
                pthread_mutex_unlock(&env_mutex);
                return(ENOSPC);
            }
            strcpy(buf, &environ[i][len+1]);
            pthread_mutex_unlock(&env_mutex);
            return 0;
        }
    }
    pthread_mutex_unlock(&env_mutex);
    return ENOENT;
}

int main(void ) {
    char* name = "LOGNAME";
    char buf[1024];
    int err;

    if ((err = getenv_r(name, buf, 1024)) != 0)
        err_exit(err, "getenv error");

    printf("LOGNAME: %s\n", buf);
}

12.4 线程与信号

每个线程都有自己的信号屏蔽字,但信号处理程序是所有线程共享的。

信号会发送给特定线程。如果是硬件相关信号,通常发送给引发事件的线程;其他信号则发送给任意线程。

如果多个线程在sigwait中等待同一个信号,只有其中一个线程会从sigwait返回。

使用kill发送信号给进程,使用pthread_kill发送信号给线程。

定时器是进程资源,所有线程共享。

示例代码:

#include <pthread.h>
#include <signal.h>
#include "apue.h"

sigset_t mask;
int quitflag;

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait_loc = PTHREAD_COND_INITIALIZER;

void* thr_fn(void* arg) {
    int err, signo;

    for(;;) {
        err = sigwait(&mask, &quitflag);   //线程等待mask中的信号
        if (err != 0)
            err_exit(err, "sigwait error");

        switch (signo) {
            case SIGINT:
                printf("quit");
                break;
            case SIGQUIT:
                pthread_mutex_lock(&lock);
                quitflag = 1;
                pthread_mutex_unlock(&lock);
                pthread_cond_signal(&wait_loc);
                return 0;
            default:
                printf("signo: %d", signo);
                exit(1);
        }

    }
}

int main(void) {
    int err;
    sigset_t oldmask;
    pthread_t tid;

    sigemptyset(&mask);
    sigaddset(&mask, SIGQUIT);  //设置信号屏蔽SIGQUIT,SIGINT
    sigaddset(&mask, SIGINT);
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
        err_exit(err, "sigmask error");

    err = pthread_create(&tid, NULL, thr_fn, NULL);  //创建新线程
    if(err != 0)
        err_exit(err, "create error");

    pthread_mutex_lock(&lock);
    while (quitflag == 0)
        pthread_cond_wait(&wait_loc, &lock);  //等待条件满足
    pthread_mutex_unlock(&lock);

    quitflag = 0;

    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0) //恢复原来的信号屏蔽字设置
        err_quit("sigprocmask error");

    exit(0);
}

12.5 线程与fork

调用fork时,子进程将复制父进程的整个地址空间。

子进程中只有一个线程,由调用fork的线程副本构成。如果父进程中的线程持有锁,子进程也将持有这些锁。

可以通过pthread_atfork注册处理函数来解决同步状态不一致的问题。

示例代码:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include "apue.h"

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void prepare(void) {
    int err;

    printf("preparing lock....\n");
    if ((err = pthread_mutex_lock(&lock1)) != 0)
        printf("cannot lock1\n");
    if ((err = pthread_mutex_lock(&lock2)) != 0)
        printf("cannot lock2\n");
}

void parent(void) {
    int err;

    printf("parent unlock....\n");
    if ((err = pthread_mutex_unlock(&lock1)) != 0)
        err_cont(err, "parent cannot lock1\n");
    if ((err = pthread_mutex_unlock(&lock2)) != 0)
        err_cont(err, "parent cannot lock2\n");
}

void child(void) {
    int err;

    printf("child unlock....\n");
    if ((err = pthread_mutex_unlock(&lock1)) != 0)
        err_cont(err, "child cannot lock1\n");
    if ((err = pthread_mutex_unlock(&lock2)) != 0)
        err_cont(err, "child cannot lock2\n");
}

void* thrd_fn(void* arg) {
    printf("thread start\n");
    pause();
    return 0;
}

int main(void ) {
    int err;
    pid_t pid;
    pthread_t tid;

  //设置线程fork处理函数,分别是fork前,fork后父进程子进程执行的操作
    if ((err = pthread_atfork(prepare, parent, child)) != 0)  
        err_exit(err, "cannot install fork handler\n");
    if ((err = pthread_create(&tid, NULL, thrd_fn, NULL)) != 0)
        err_exit(err, "cannot create thread\n");

    sleep(2);
    printf("parent about fork....\n");
    if ((pid = fork()) < 0)
        err_sys("fork error\n");
    else if (pid == 0)
        printf("child return from fork");
    else
        printf("parent return from fork");

    exit(0);
}

12.6 线程与IO

pread和pwrite允许偏移量设定和数据读取作为一个原子操作,适用于多线程环境。所有线程共享相同的文件描述符。

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Node.js 中的异步编程:回调与 Promise

Node.js 是一个基于 JavaScript 构建的单线程、非阻塞运行环境,它通过异步编程机制来高效处理多个操作。在执行如文件读取、API 请求或数据库查询等任务时,Node.js 不会等待这些操作完成,而是使用回调函数和 Promise 来避免阻塞主线程。 回调方式实现异步 那么当异步操作完成后,Node.js 如何知道接下来要做什么呢?这就要用到 回调函数(callback)。 回调本质上...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

Android 中 EventBus 的通信机制与实现原理深度解析

EventBus 核心设计思想 EventBus 是一个基于观察者模式的事件总线框架,广泛应用于 Android 平台以实现组件解耦。它通过中心化的消息分发机制,使不同层级、不同线程的对象能够以"发布-订阅"方式通信,避免了传统接口回调或广播带来的强依赖问题。 核心角色说明 事件(Event):任意 Java 对象,作为数据载体,如网络状态变更通知、用户登录信息等。 发布者(Publi...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。