(万字长文)C语言多线程之threads函数库

本文介绍了C语言中threads函数库的使用,包括musl库的安装与配置,线程的创建、加入、脱离与终止,互斥体的创建、锁定、解锁与销毁,条件变量的创建、阻塞与唤醒,以及线程局域存储的使用。示例代码展示了如何在实际编程中应用这些概念,帮助读者深入理解C语言的多线程编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

threads函数库的简介

介绍

musl的安装与使用

musl的安装

musl的使用

线程的使用

线程的创建

线程的加入与脱离

线程的加入

线程的脱离

线程的终止

举个例子

互斥体

互斥体的创建

互斥体的使用

互斥体的锁定

互斥体的解锁

互斥体的销毁

互斥体类型

平常互斥体

死锁

递归互斥体

定时互斥体

定时互斥体的锁定

timespec结构体

定时互斥体的使用

条件变量

条件变量的创建

条件变量的使用

条件变量的阻塞

条件变量的唤醒

唤醒一个线程

唤醒所有线程

条件变量的销毁

举个例子

线程局域存储

存储键

存储键的创建

存储键的使用

存储键的写入

存储键的读取

存储键的销毁

举个例子

声明


threads函数库的简介

介绍

threads函数库是c11标准新加的多线程函数库,使用时,要包含threads.h头文件,编译时规定std=c11

我们都知道,程序运行时会产生进程,程序会被加载到进程的地址空间中。但是,进程还要有执行单位,那就是线程。进程创建时,肯定要有至少一个线程执行代码。进程刚创建时创建的线程叫主线程,主线程创建的线程叫子线程。

glibc在内的大部分标准库都不支持使用这个函数库,不支持这个函数库的编译器会定义__STDC_NO_THREADS__宏。而musl的标准库支持这个函数库。因此,如无特殊说明,本文使用的编译器均是musl-gcc。

musl的安装与使用

musl的安装

在ubuntu中,我们可以采用apt安装这个标准库。在终端中输入:

sudo apt install musl
sudo apt install musl-tools

就可以安装musl标准库了。

我们可以输入:

musl-gcc --version

检测是否安装成功。musl-gcc是采用musl函数库的gcc编译器,如果安装成功,会看到:

gcc (Ubuntu 5.5.0-12ubuntu1~16.04) 5.5.0 20171010
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

musl的使用

使用这个标准库非常简单,只需要输入:

musl-gcc ...

就可以了。使用的方法类似普通gcc的使用。

线程的使用

线程的创建

线程的创建采用thrd_create(3)函数,这个函数的原形如下:

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

这个函数将会创建一个运行func函数的线程,在thrd_create(3)函数里,arg将会作为func函数的参数。如果线程创建成功,thr指向的对象将被设置为所创建线程的标识符。这个函数完成的同时,所创建的线程将开始。

以下是对其参数的详细解释:

  • thr: 指向将要存放线程标识符的地址
  • func: 需要执行的函数。注意:这个函数是int (*)(void *)类型的,这意味着函数的返回值是int类型,接受一个void *类型的参数。
  • arg: 向执行函数传递的参数

这个函数的返回值是一个枚举,其定义如下:

enum {

    thrd_success = /* unspecified */,
    thrd_nomem = /* unspecified */,
    thrd_timedout = /* unspecified */,
    thrd_busy = /* unspecified */,
    thrd_error = /* unspecified */
    
};

其中,thrd_success是线程创建成功的返回值。thrd_nomem表示因地址空间不足造成线程创建失败。thrd_error表示其他错误发生。这个枚举类型的其他枚举形式不会返回。

另外,当线程结束、加入或脱离后,线程标识符可能被重新使用。

当线程返回,或调用thrd_exit(3)时,线程结束。线程的结束码会被设为执行函数的返回值或thrd_exit(3)函数的参数。

线程的加入与脱离

当线程结束、加入或脱离后,线程标识符可能被重新使用。那么,什么是加入、脱离呢?如何操作?

线程的加入

线程的加入意味着阻塞当前的线程直到线程结束。通过thrd_join(3)函数可以实现线程的加入。

int thrd_join( thrd_t thr, int *res );

这个函数与线程同时终止,其参数如下:

  • thr:要进行加入操作的线程
  • res:当线程结束时,将线程的结束码放入该参数指向的地址中。这个参数可以为NULL。

这个函数的返回值仍然是上文所说的枚举。如果成功,则返回thrd_success,否则返回thrd_error。

线程的脱离

线程的脱离与加入相反,意味着将线程同当前环境脱离出来,当线程结束时,将自动销毁,而不会阻塞线程。通过thrd_detach(3)实现线程脱离。

int thrd_detach( thrd_t thr );

这个函数的参数只有一个,即thr,表示要进行脱离操作的线程。这个函数的返回值仍然是上文所说的枚举。如果成功,则返回thrd_success,否则返回thrd_error。

线程的终止

在以下情况下,线程终止:

  • 执行函数调用thrd_exit(3)函数;
  • 执行函数返回,这相当于调用thrd_exit(3)函数;
  • 本进程中任何线程调用exit(3)函数,或main函数返回,这将会使本进程所有线程终止。

接下来给大家介绍thrd_exit(3)函数,在C11标准中,其原型如下:

_Noreturn void thrd_exit( int res );

在C23标准中,其原型如下:

[[noreturn]] void thrd_exit( int res );

res表示结束码,这将会成为thrd_join(3)的返回值。

exit(3)是标准库中的函数,这里也给大家介绍一下:

void exit(int status);

使用时需要包含stdlib.h头文件。

这个函数非常简单。它会导致进程终止。并且status参数与上0377的结果将会返回给父进程。为什么要要与上一个0377?因为子进程的状态改变不只有终止,还有被信号停止、被信号重新开始。为了使系统及用户区分,因此需要进行位运算。

事实上,exit(0)等价于main函数中return 0.这说明main函数返回时也会与上0377.所以,如果不知道位操作,可以简单的把exit看成main函数的返回。

举个例子

以下程序展示了如何创建一个线程,输出Hello World:

#include <stdio.h>

#ifndef __STDC_NO_THREADS__
#include <threads.h>
#endif

int func(void *args){
    printf("Hello World\n");
    return 0;
}

int main(void){
    #ifdef __STDC_NO_THREADS__
    printf("cannot use threads\n");
    #else
    thrd_t th;
    thrd_create(&th, func, NULL);
    thrd_join(th, NULL);
    #endif
    return 0;
}

如果__STDC_NO_THREADS__宏被定义,则这个程序的流程如下:

  1. 定义函数 func

    • 这个函数打印"Hello World"并返回0。
  2. main 函数:

    • 定义一个线程变量th
    • 使用thrd_create函数创建一个新线程,执行函数是func,并且没有传递任何参数。
    • 使用thrd_join进行线程的加入操作,等待线程完成。
  3. 返回0,结束程序。

这个程序对__STDC_NO_THREADS__宏进行了多次检测,如果没有这个宏,就不能使用threads库。这样的写法是为了更好的兼容跨平台程序。

我们使用musl编译:

musl-gcc -std=c11 th1.c
./a.out

输出:

Hello World

可以看到程序成功输出了Hello World。使用threads库可以更好的支持跨平台应用。

如果我们把程序的thrd_join改成thrd_detach:

#include <stdio.h>

#ifndef __STDC_NO_THREADS__
#include <threads.h>
#endif

int func(void *args){
    printf("Hello World\n");
    return 0;
}

int main(void){
    #ifdef __STDC_NO_THREADS__
    printf("cannot use threads\n");
    #else
    thrd_t th;
    thrd_create(&th, func, NULL);
    thrd_detach(th);
    #endif
    return 0;
}

 则不会有输出。这是因为线程创建后main函数就返回了,线程终止。

互斥体

互斥体又叫互斥锁,可以解决多个线程同时访问一块地址的问题。先看一段代码:

#include <threads.h>
#include <stdio.h>

int test = 10;

int func(void *args){
    while(--test > 0){
        printf("%d\n", test);
    }
    retu
MySQL多数据源是指在一个应用程序中同时使用多个不同的MySQL数据库来存储和管理数据的技术。它可以帮助开发人员更灵活地处理各种数据库操作,提高程序的性能和可扩展性。下面是一个完整的MySQL多数据源教程。 一、设置数据库连接信息 1. 在应用程序的配置文件中,创建多个数据库连接的配置项。例如,可以为每个数据源创建一个配置项,分别命名为db1、db2等。 2. 在配置项中,设置每个数据源的连接信息,包括数据库地址、用户名、密码等。 二、创建数据源管理器 1. 创建一个数据源管理器类,用于管理多个数据源。该类需要实现数据源的动态切换和获取。 2. 使用Java的线程安全的数据结构,如ConcurrentHashMap来存储数据源信息。将配置文件中的数据库连接信息加载到数据结构中。 3. 实现方法来切换不同的数据源,通过传入数据源的名称来切换到对应的数据库。 三、实现数据源切换 1. 在应用程序中,根据业务需求选择需要使用的数据源。可以通过调用数据源管理器的方法来切换数据源。 2. 在DAO层的代码中,根据当前使用的数据源名称,选择对应的数据源进行数据库操作。 四、使用多数据源进行数据库操作 1. 在DAO层的代码中,区分不同的数据源,并将数据库操作的代码包装在对应的数据源中。 2. 在业务层的代码中,调用DAO层的方法来进行数据库操作。不同的数据源会自动切换。 五、处理事务 1. 如果需要在一个事务中操作多个数据源,可以使用分布式事务的方式来处理。 2. 可以使用开源的分布式事务框架,如Atomikos、Bitronix等来实现多数据源的事务管理。 六、监控和维护 1. 使用监控工具来监控多个数据源的使用情况,包括连接数、查询次数等。 2. 定期对数据库进行维护,包括索引优化、数据清理等工作,以保证数据库的性能和稳定性。 通过以上步骤,我们可以实现MySQL多数据源的配置和使用。使用多数据源可以更好地管理和处理不同的数据库操作,在提高程序性能和可扩展性的同时,也提供了更灵活的数据操作方式。同时,需要注意合理选择和配置数据源,以及监控和维护数据库,以保证系统的运行效率和数据的安全性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值