OS74.【Linux】线程互斥(3) 线程安全、重入

目录

1.线程安全

定义

线程不安全

[MIT大学的6.005 --- Software Construction课程的讲义有对线程安全的定义](#MIT大学的6.005 — Software Construction课程的讲义有对线程安全的定义)

[Reading 20: Thread Safety(阅读20: 线程安全)](#Reading 20: Thread Safety(阅读20: 线程安全))

[What Threadsafe Means(线程安全的含义)](#What Threadsafe Means(线程安全的含义))

2.重入的定义

定义

可重入和不可重入

3.线程不安全的情况

不保护共享变量的函数

STL库不是线程安全的

函数状态随着被调用,状态发生变化的函数

返回指向静态变量指针的函数

4.线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

解释什么是二义性

[QT文档: C++的类通常是线程安全的](#QT文档: C++的类通常是线程安全的)

Reentrancy

5.不可重入的情况

调用了malloc/free函数

调用了标准I/O库函数

可重入函数体内使用了静态的数据结构

[IBM公司AIX 7.2.0对(不)可重入函数的说明](#IBM公司AIX 7.2.0对(不)可重入函数的说明)

Reentrance(重入)

分析strtok为什么不可重入

代码演示1

strtok_r

代码演示2

结论

分析ctime为什么不可重入

ctime的调用链

6.可重入的情况

不使用全局变量或静态变量

不使用用malloc或者new开辟出的空间

不调用不可重入函数

不返回静态或全局数据,所有数据都有函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

7.线程安全和重入的区别

核心概念


1.线程安全

定义

线程安全: 多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或者静态变量进行操作

可以得出线程不安全的定义:

线程不安全

多个线程并发访问同一段代码,没有锁保护,会出现线程不安全的问题

MIT大学的6.005 --- Software Construction课程的讲义有对线程安全的定义

https://web.mit.edu/6.005/www/fa15/classes/20-thread-safety/

Reading 20: Thread Safety(阅读20: 线程安全)

What Threadsafe Means(线程安全的含义)

A data type or static method is threadsafe if it behaves correctly when used from multiple threads, regardless of how those threads are executed, and without demanding additional coordination from the calling code.

(

翻译: 如果一个数据类型或静态方法在多个线程同时使用时能够表现出正确的行为,无论这些线程如何执行,且不需要调用方进行额外的协调,那么它就是线程安全的

)

  • "behaves correctly" means satisfying its specification and preserving its rep invariant;
  • "regardless of how threads are executed" means threads might be on multiple processors or timesliced on the same processor;
  • "without additional coordination" means that the data type can't put preconditions on its caller related to timing, like "you can't call get() while set() is in progress."

(

翻译:

  • "表现出正确的行为" 指的是满足其规约并维护表示不变量

  • "无论线程如何执行" 意味着线程可能在多个处理器上并行运行,也可能在同一个处理器上分时执行

  • "无需额外协调" 指的是该数据类型不能对调用方设置与时间相关的先决条件,例如"不能在执行 set() 的过程中调用 get()"

)

Remember Iterator? It's not threadsafe. Iterator's specification says that you can't modify a collection at the same time as you're iterating over it. That's a timing-related precondition put on the caller, and Iterator makes no guarantee to behave correctly if you violate it.

(

翻译: 还记得迭代器(Iterator)吗? 它不是线程安全的. Iterator 的规约明确规定,在遍历集合的同时不能修改该集合. 这是一个对调用方设置的与时间相关的先决条件,如果你违反了这一条件,Iterator 不保证会表现出正确的行为

)

2.重入的定义

定义

之前在OS68.【Linux】pthread线程库的使用文章的代码出现过函数重入的情况

重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入

可重入和不可重入

可重入函数(Reentrant Function): 一个函数在重入的情况下,运行结果不会出现任何不同,也不会出现其它任何问题

不可重入函数(Non-reentrant Function): 一个函数在重入的情况下,运行结果会出现问题

大多数函数都是不可以重入的

从以上信息可以推出: 可重入的函数被多个线程访问一定不会出现线程安全问题; 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题; 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

结论: 线程安全不一定是可重入的,而可重入函数则一定是线程安全的

3.线程不安全的情况

不保护共享变量的函数

例如多线程在线程函数中修改同一个vector对象,显然这个vector对象被所有线程共享,为了保证vector对象的安全,程序员需要自己对vector对象加锁,注意: STL库本身不是线程安全的

STL库不是线程安全的

STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响

而且对于不同的容器, 加锁方式的不同, 性能可能也不同
因此STL 默认不是线程安全的 (读者也可以自己去查看源码),如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全

函数状态随着被调用,状态发生变化的函数

举个例子

cpp 复制代码
void* thread_routine(void* args)
{
    static int cnt=0;//cnt只会初始化一次
    cnt++;
    //......
}

每调用一次thread_routine,cnt的值都会+1,cnt的值在变化,显然thread_routine符合"函数状态随着被调用,状态发生变化的函数"

返回指向静态变量指针的函数

举个例子:

cpp 复制代码
#include <pthread.h>
#include <cstdio>
#define NUM 10
void* thread_routine(void*)
{
    static int cnt=0;//cnt只会初始化一次
    cnt++;
    printf("tid=%ld cnt=%d cnt的地址: %p\n",pthread_self(),cnt,&cnt);
    return &cnt;
}

int main()
{
    pthread_t tid_arr[NUM];
    for (int i=0;i<NUM;i++)
    {
        pthread_create(&tid_arr[i],nullptr,thread_routine,nullptr);
    }
    for (int i=0;i<NUM;i++)
    {
        void* cnt_ptr;
        pthread_join(tid_arr[i],&cnt_ptr);
        printf("成功等待tid=%ld的新线程: cnt=%d cnt的地址: %p\n",tid_arr[i],*((int*)cnt_ptr),cnt_ptr);
    }
    return 0;
}

cnt是静态变量,而且静态变量在程序的整个生命周期内只存在一份实例,被所有线程共享

运行结果: 主线程和新线程输出的cnt的地址都一样,符合静态变量只存在一份实例,被所有线程共享

但是从运行结果来看,cnt没有被保护,所以新线程输出的cnt的值和主线程得到的cnt的值不一样:

因此返回指向静态变量指针的函数不是线程安全的,但注意: 返回静态变量的指针指向的静态变量可能是线程安全的,如果该静态变量被__thread修饰的话,这个在之后的OS92.【Linux】编写简单的UDP服务端-CSDN博客文章重点强调过

4.线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

不能说明二义性的情况

例如以下代码:

cpp 复制代码
#include <pthread.h>
#include <cstdio>
#define NUM 10
int cnt=0;
void* thread_routine(void*)
{
    cnt++;
    printf("新线程: cnt=%d\n",cnt);
    pthread_detach(pthread_self());
    return nullptr;
}

int main()
{
    pthread_t tid_arr[NUM];
    for (int i=0;i<NUM;i++)
    {
        pthread_create(&tid_arr[i],nullptr,thread_routine,nullptr);
    }
    printf("主线程: cnt=%d\n",cnt);
    return 0;
}

运行结果: 每次运行结果不一样

++以上代码不能说明二义性++ ,每次运行结果不一样看似是二义性,但是"多个线程之间的切换不会导致该接口的执行结果存在二义性"强调的是执行结果

什么是执行结果呢? 得先执行完,然后看到结果,你表现给我,你对于这个执行结果的认知就是程序之后之后打印结果不同就是二义性,没有去考虑函数是否执行完了,结果是看打印结果还是看临界变量的值. 就比如对一个全局变量自增,每个线程自增1000次,执行完完了之后,主线程进行打印,如果是两个线程执行的话,如果每次的运行结果都是2000,说明没有二义性

以上代码的主线程都不在意其他线程是否创建成功,是否开始执行,是否执行结束,然后结束进程,这样的话,没有启动的线程,启动了但没有被及时调度的线程还没有执行函数的逻辑呢,也就是说函数都没有开始执行无法说明执行结果出现二义性

下文的"分析strtok为什么不可重入"和"分析ctime为什么不可重入"就能说明二义性

QT文档: C++的类通常是线程安全的

QT文档https://doc.qt.io/qt-6/threads-reentrancy.html是这样说的:

Reentrancy

C++ classes are often reentrant, simply because they only access their own member data. Any thread can call a member function on an instance of a reentrant class, as long as no other thread can call a member function on the same instance of the class at the same time. For example, the Counter class below is reentrant:

(

翻译: C++ 类通常是可重入的,这仅仅是因为它们只访问自己的成员数据.

只要没有其他线程同时调用该类的同一个实例的成员函数,任何线程都可以调用可重入类的实例的成员函数

例如,下面的 Counter 类就是可重入的:

)

cpp 复制代码
class Counter
{
public:
    Counter() { n = 0; }

    void increment() { ++n; }
    void decrement() { --n; }
    int value() const { return n; }

private:
    int n;
};

The class isn't thread-safe, because if multiple threads try to modify the data member n, the result is undefined. This is because the ++ and -- operators aren't always atomic. Indeed, they usually expand to three machine instructions:

  1. Load the variable's value in a register.
  2. Increment or decrement the register's value.
  3. Store the register's value back into main memory.

If thread A and thread B load the variable's old value simultaneously, increment their register, and store it back, they end up overwriting each other, and the variable is incremented only once!

(

翻译:这个类不是线程安全的,因为如果多个线程试图修改数据成员 n,结果将是未定义的,这是因为 ++ 和 -- 运算符并不总是原子操作

实际上,它们通常会展开为三条机器指令:

  1. 将变量的值加载到寄存器中

  2. 对寄存器的值进行递增或递减

  3. 将寄存器的值存回主内存

如果线程 A 和线程 B 同时加载变量的旧值,各自递增寄存器后再存回内存,它们最终会相互覆盖,导致该变量只被递增了一次

)

5.不可重入的情况

调用了malloc/free函数

malloc函数是用全局链表来管理堆的

调用了标准I/O库函数

标准I/O库的很多实现都以不可重入的方式使用全局数据结构

可重入函数体内使用了静态的数据结构

IBM公司AIX 7.2.0对(不)可重入函数的说明

https://www.ibm.com/docs/en/aix/7.3.0?topic=programming-writing-reentrant-threadsafe-code

Reentrance(重入)

A reentrant function does not hold static data over successive calls, nor does it return a pointer to static data. All data is provided by the caller of the function. A reentrant function must not call non-reentrant functions.

(

翻译: 可重入函数不会在连续调用之间保持静态数据,也不会返回指向静态数据的指针. 所有数据都由函数的调用者提供. 可重入函数绝不能调用不可重入函数.

)

A non-reentrant function can often, but not always, be identified by its external interface and its usage. For example, the strtok subroutine is not reentrant, because it holds the string to be broken into tokens. The ctime subroutine is also not reentrant; it returns a pointer to static data that is overwritten by each call.

(

翻译: 不可重入函数通常(但并非总是)可以通过其外部接口和使用方式来识别

例如,strtok 是不可重入的,因为它保存了待分割的字符串,ctime 也是不可重入的,它返回一个指向静态数据的指针,该数据会被每次调用所覆盖

)

上面提到了strtok和ctime都是不可重入的,下面逐个分析

分析strtok为什么不可重入

strtok之前在56.【C语言】字符函数和字符串函数(strtok函数)文章讲过:

cpp 复制代码
char * strtok ( char * str, const char * delimiters );

delimiters参数指向一个字符串,定义了用作分隔符的字符集合

• 第一个参数指定一个字符串,它包含了0个或者多个由delimiters字符串中一个或者多个分隔符分割的标记

• strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针(注:strtok函数会改变被操作的字符串,所以被strtok函数切分的字符串一般都是临时拷贝(如:一份原字符串arr,一份拷贝字符串copy)的内容并且可修改(不能用const修饰))

• strtok函数的第一个参数不为NULL,函数将找到str中第一个标记,strtok函数++将保存它在字符串中的位置++

• strtok函数的第一个参数为NULL,函数将在同一个字符串中++被保存的位置++ 开始,查找下一个标记

• 如果字符串中不存在更多的标记,则返回NULL 指针

例如多次拆分字符串

cpp 复制代码
#include <stdio.h>
#include <cstring>
int main()
{
	char arr[20] = { "user@zhangcoder.net" };
	char delimiters[20] = { "@." };
	char copy[20] = { 0 };
	strcpy(copy, arr);
    char* p = strtok(copy, delimiters);
	printf("%s\n", p);
	p = strtok(NULL, delimiters);
	printf("%s\n", p);
	p = strtok(NULL, delimiters);
	printf("%s\n", p);
	return 0;
}

提问: 上文所说的"++被保存的位置++"存储在哪里?

堆区?栈区?静态区?全局数据区? 看源码!

在glibc-2.42的/string/strtok.c中定义了strtok函数:

cpp 复制代码
#include <string.h>


/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the last string strtok() was called with is
   used.  For example:
	char s[] = "-abc-=-def";
	x = strtok(s, "-");		// x = "abc"
	x = strtok(NULL, "-=");		// x = "def"
	x = strtok(NULL, "=");		// x = NULL
		// s = "abc\0=-def\0"
*/
char *
strtok (char *s, const char *delim)
{
  static char *olds;//static!!!
  return __strtok_r (s, delim, &olds);
}

这个olds就是++保存的下一次开始截取的位置,是静态变量! 所有线程都能访问到++

代码演示1

每个线程都拆分主线程提供的唯一的字符串,拆分好的字符串写入自己新建的文件中

cpp 复制代码
//线程不安全
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define NUM 10
//依次交给thread-0~thread-9
char str_arr[][256] ={"user@zhangcoder.net",\
                 "host=localhost;port=3306,user=root,password=123456",\
                 "192.168.1.1@node1;192.168.1.2@node2,10.0.0.1@node3",\
                 "/home/user/file.txt@local",\
                 "https=api.zhangcoder.net;",\
                 "db.host=127.0.0.1",\
                 "2024.03.15@14:30:00;INFO",\
                 "zhangsan=25;",
                 "a.b@c=d",\
                 "tag#data"};

char delimiters[] = ";,@#=.-/";

class thread_data
{
public:
    thread_data(char* str,std::string* file_name)
    :_str(str)
    ,_file_name(file_name)
    {}
    char* _str;
    std::string* _file_name;
};

void* thread_routine(void* args)
{
    thread_data* obj_ptr=static_cast<thread_data*>(args);
    char* str=obj_ptr->_str;
    std::string* file_name=obj_ptr->_file_name;
    int fd = open((*file_name).c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
    strtok(str, delimiters);
    while (str != nullptr) 
    {
        dprintf(fd, "%s ", str);
        str = strtok(nullptr, delimiters);  // 继续获取下一个
    }
    dprintf(fd, "\n");
    close(fd);
    return nullptr;
}

int main()
{
	pthread_t tid_arr[NUM];
    for (int i=0;i<NUM;i++)
    {
        //简单起见,不释放
        std::string* file_name=new std::string("thread-"+std::to_string(i));
        thread_data* obj_ptr=new thread_data(str_arr[i],file_name);
        pthread_create(&tid_arr[i],nullptr,thread_routine,obj_ptr);
    }
    for (int i=0;i<NUM;i++)
        pthread_join(tid_arr[i],nullptr);
	return 0;
}

运行结果:

每个线程都将截取的结果写入到自己的文件,一共10个:

有些字符串截取的是对的有些是错的,例如下面图片的"host localhost 03 15 INFO"

strtok_r

为了解决strtok线程不安全的问题,POSIX标准提出了strtok_r函数(C标准没有!!!):

cpp 复制代码
char *strtok_r(char *str, const char *delim, char **saveptr);

strtok_r比strtok多了一个char **saveptr参数,手册是这样说的:

strtok_r是strtok的可重入版本(reentrant version),saveptr用于保存下一次字符串截取的位置的指针

在glibc-2.42的/string/strtok_r.c有定义:

如果定义了_LIBC,那么strtok_r就是__strtok_r

cpp 复制代码
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <string.h>

#ifndef _LIBC
/* Get specification.  */
# include "strtok_r.h"
# define __strtok_r strtok_r
#endif

__strtok_r的定义: 在__strtok_r内部没有使用static修饰的静态变量,而是"the saved pointer in SAVE_PTR is used as the next starting point"

cpp 复制代码
/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the saved pointer in SAVE_PTR is used as
   the next starting point.  For example:
	char s[] = "-abc-=-def";
	char *sp;
	x = strtok_r(s, "-", &sp);	// x = "abc", sp = "=-def"
	x = strtok_r(NULL, "-=", &sp);	// x = "def", sp = NULL
	x = strtok_r(NULL, "=", &sp);	// x = NULL
		// s = "abc\0-def\0"
*/
char *
__strtok_r (char *s, const char *delim, char **save_ptr)
{
  char *end;

  if (s == NULL)
    s = *save_ptr;

  if (*s == '\0')
    {
      *save_ptr = s;
      return NULL;
    }

  /* Scan leading delimiters.  */
  s += strspn (s, delim);
  if (*s == '\0')
    {
      *save_ptr = s;
      return NULL;
    }

  /* Find the end of the token.  */
  end = s + strcspn (s, delim);
  if (*end == '\0')
    {
      *save_ptr = end;
      return s;
    }

  /* Terminate the token and make *SAVE_PTR point past it.  */
  *end = '\0';
  *save_ptr = end + 1;
  return s;
}
#ifdef weak_alias
libc_hidden_def (__strtok_r)
weak_alias (__strtok_r, strtok_r)
#endif
代码演示2
cpp 复制代码
char *strtok_r(char *str, const char *delim, char **saveptr);

saveptr用于保存字符串之前截取位置的指针,那么每个线程都需要一个私有的saveptr,可以这样写:

cpp 复制代码
__thread char* save_ptr=nullptr;

注: __thread之前在OS72.【Linux】__thread、分离线程文章讲过

那么有:

cpp 复制代码
//线程安全
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define NUM 10
//依次交给thread-0~thread-9
char str_arr[][256] ={"user@zhangcoder.net",\
                 "host=localhost;port=3306,user=root,password=123456",\
                 "192.168.1.1@node1;192.168.1.2@node2,10.0.0.1@node3",\
                 "/home/user/file.txt@local",\
                 "https=api.zhangcoder.net;",\
                 "db.host=127.0.0.1",\
                 "2024.03.15@14:30:00;INFO",\
                 "zhangsan=25;",
                 "a.b@c=d",\
                 "tag#data"};

char delimiters[] = ";,@#=.-/";
__thread char* save_ptr=nullptr;
class thread_data
{
public:
    thread_data(char* str,std::string* file_name)
    :_str(str)
    ,_file_name(file_name)
    {}
    char* _str;
    std::string* _file_name;
};

void* thread_routine(void* args)
{
    thread_data* obj_ptr=static_cast<thread_data*>(args);
    char* str=obj_ptr->_str;
    std::string* file_name=obj_ptr->_file_name;
    int fd = open((*file_name).c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
    strtok_r(str, delimiters,&save_ptr);
    while (str != nullptr) 
    {
        dprintf(fd, "%s ", str);
        str = strtok_r(nullptr, delimiters,&save_ptr);  // 继续获取下一个
    }
    dprintf(fd, "\n");
    close(fd);
    return nullptr;
}

int main()
{
	pthread_t tid_arr[NUM];
    for (int i=0;i<NUM;i++)
    {
        //简单起见,不释放
        std::string* file_name=new std::string("thread-"+std::to_string(i));
        thread_data* obj_ptr=new thread_data(str_arr[i],file_name);
        pthread_create(&tid_arr[i],nullptr,thread_routine,obj_ptr);
    }
    for (int i=0;i<NUM;i++)
        pthread_join(tid_arr[i],nullptr);
	return 0;
}

运行结果:每个线程截取的字符串都是对的

回过来看,这下就能理解为什么glibc-2.42的strtok函数内为什么要调用__strtok_r了,这是代码复用,能减小代码冗余,提高开发效率

cpp 复制代码
#include <string.h>
char *
strtok (char *s, const char *delim)
{
  static char *olds;
  return __strtok_r (s, delim, &olds);
}
结论

1.strtok内部使用的静态变量,是不可重入函数

2.strtok是线程不安全的"MT-Unsafe race:strtok",strtok_r是线程安全的"MT-Safe"

man strtok:

分析ctime为什么不可重入

cpp 复制代码
char *ctime(const time_t *timep);

作用: 将时间戳转换为一个表示当地时间的字符串(人类可读形式),并返回这个字符串

例如以下代码:

cpp 复制代码
#include <iostream>
#include <ctime>
int main ()
{
   time_t curtime=time(nullptr);
   time(&curtime);
   std::cout<<"当前时间为"<<ctime(&curtime);
   return 0;
}

运行结果:

在glibc-2.42的/time/ctime.c中定义了ctime函数

cpp 复制代码
/* Copyright (C) 1991-2025 Free Software Foundation, Inc.
   This file is part of the GNU C Library.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <https://www.gnu.org/licenses/>.  */

#include <time.h>

/* Return a string as returned by asctime which
   is the representation of *T in that form.  */
char *
__ctime64 (const __time64_t *t)
{
  /* The C Standard says ctime (t) is equivalent to asctime (localtime (t)).
     In particular, ctime and asctime must yield the same pointer.  */
  return asctime (__localtime64 (t));
}

/* Provide a 32-bit variant if needed.  */

#if __TIMESIZE != 64

libc_hidden_def (__ctime64)

char *
ctime (const time_t *t)
{
  __time64_t t64 = *t;
  return __ctime64 (&t64);
}

#endif

asctime定义在/time/asctime.c中:

cpp 复制代码
/* Returns a string of the form "Day Mon dd hh:mm:ss yyyy\n"
   which is the representation of TP in that form.  */
char *
asctime (const struct tm *tp)
{
  return asctime_internal (tp, result, sizeof (result));
}
libc_hidden_def (asctime)

asctime_internal定义在/time/asctime.c中:

cpp 复制代码
static char *
asctime_internal (const struct tm *tp, char *buf, size_t buflen)
{
  if (tp == NULL)
    {
      __set_errno (EINVAL);
      return NULL;
    }

  /* We limit the size of the year which can be printed.  Using the %d
     format specifier used the addition of 1900 would overflow the
     number and a negative value is printed.  For some architectures we
     could in theory use %ld or an evern larger integer format but
     this would mean the output needs more space.  This would not be a
     problem if the 'asctime_r' interface would be defined sanely and
     a buffer size would be passed.  */
  if (__glibc_unlikely (tp->tm_year > INT_MAX - 1900))
    {
    eoverflow:
      __set_errno (EOVERFLOW);
      return NULL;
    }

  int n = __snprintf (buf, buflen, format,
		      (tp->tm_wday < 0 || tp->tm_wday >= 7
		       ? "???" : ab_day_name (tp->tm_wday)),
		      (tp->tm_mon < 0 || tp->tm_mon >= 12
		       ? "???" : ab_month_name (tp->tm_mon)),
		      tp->tm_mday, tp->tm_hour, tp->tm_min,
		      tp->tm_sec, 1900 + tp->tm_year);
  if (n < 0)
    return NULL;
  if (n >= buflen)
    goto eoverflow;

  return buf;
}

从该函数的开头static char * asctime_internal (const struct tm *tp, char *buf, size_t buflen)就会发现,返回的是static char *,是静态类型!

ctime的调用链

ctime → asctime → asctime_internal

发现asctime_internal 返回一个静态的buf,这个buf是之前asctime提供的result,result并没有在asctime内定义,而是在asctime的外部定义:

cpp 复制代码
//在/time/asctime.c内
static const char format[] = "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n";
static char result[		 3+1+ 3+1+20+1+20+1+20+1+20+1+20+1 + 1];

所以ctime是线程不安全的,但是ctime_r是线程安全的:

buf是独立的缓冲区,不会被其它线程干扰

cpp 复制代码
char *ctime_r(const time_t *timep, char *buf);

结论: ctime是线程不安全的"MT-Unsafe race:tmbuf race:asctime env locale",ctime_r是线程安全的"MT-Safe env locale"

man ctime:

6.可重入的情况

从不可重入的情况反过来想可重入的情况:

不使用全局变量或静态变量

全局变量或静态变量在整个生命周期内只存在一份实例,被所有线程共享

不使用用malloc或者new开辟出的空间

malloc和new在堆区上开辟空间,所有线程都可以访问堆区的内容

不调用不可重入函数

不返回静态或全局数据,所有数据都有函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

7.线程安全和重入的区别

线程安全是描述线程并发问题的,重入是描述函数特点的,换句话说释放可重入是函数本身的性质

可重入函数是线程安全函数的一种

核心概念

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

相关推荐
爱喝水的鱼丶9 小时前
SAP-ABAP:数据类型与数据对象 第二篇:底层逻辑篇——数据类型的分类体系与底层存储原理
运维·开发语言·学习·sap·abap
志栋智能9 小时前
效率革命:超自动化巡检如何将小时压缩为分钟?
运维·数据库·自动化
肥胖小羊9 小时前
基于状态机的客户生命周期流转与自动化触达引擎实现
开发语言·python
玄泽幻库9 小时前
【主流版本】JDK安装版下载地址和环境配置方法
java·开发语言·jdk
夹芯饼干9 小时前
CentOS 7 虚拟机联网与 yum 源配置笔记
linux·笔记·centos
十年编程老舅9 小时前
Linux NUMA架构深度剖析:内存管理、进程调度与性能优化
linux·数据库·c++·内存管理·numa
西凉的悲伤9 小时前
Java parallelStream并行流
java·开发语言·parallelstream·并行流
少司府9 小时前
C++基础入门:深挖list的那些事
开发语言·数据结构·c++·容器·list·类型转换·类和对象
MilesShi9 小时前
UI 自动化的基本功:元素定位的原则、策略与实战经验
运维·ui·自动化