Main Content

CWE Rule 366

Race Condition within a Thread

Since R2023a

Description

Rule Description

If two threads of execution use a resource simultaneously, there exists the possibility that resources may be used while invalid, in turn making the state of execution undefined.

Polyspace Implementation

The rule checker checks for these issues:

  • Atomic load and store sequence not atomic

  • Atomic variable accessed twice in an expression

  • Data race

  • Data race on adjacent bit fields

  • Data race through standard library function call

Examples

expand all

Issue

This checker is deactivated in a default Polyspace® as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when you use these functions to load, and then store an atomic variable.

  • C functions:

    • atomic_load()

    • atomic_load_explicit()

    • atomic_store()

    • atomic_store_explicit()

  • C++ functions:

    • std::atomic_load()

    • std::atomic_load_explicit()

    • std::atomic_store()

    • std::atomic_store_explicit()

    • std::atomic::load()

    • std::atomic::store()

A thread cannot interrupt an atomic load or an atomic store operation on a variable, but a thread can interrupt a store, and then load sequence.

Risk

A thread can modify a variable between the load and store operations, resulting in a data race condition.

Fix

To read, modify, and store a variable atomically, use a compound assignment operator such as +=, atomic_compare_exchange() or atomic_fetch_*-family functions.

Example — Loading Then Storing an Atomic Variable

#include <stdatomic.h>
#include <stdbool.h>

static atomic_bool flag = ATOMIC_VAR_INIT(false);

void init_flag(void)
{
    atomic_init(&flag, false);
}

void toggle_flag(void)
{
    bool temp_flag = atomic_load(&flag);
    temp_flag = !temp_flag;
    atomic_store(&flag, temp_flag);  //Noncompliant
}

bool get_flag(void)
{
    return atomic_load(&flag);
}

In this example, variable flag of type atomic_bool is referenced twice inside the toggle_flag() function. The function loads the variable, negates its value, then stores the new value back to the variable. If two threads call toggle_flag(), the second thread can access flag between the load and store operations of the first thread. flag can end up in an incorrect state.

Correction — Use Compound Assignment to Modify Variable

One possible correction is to use a compound assignment operator to toggle the value of flag. The C standard defines the operation by using ^= as atomic.


#include <stdatomic.h>
#include <stdbool.h>

static atomic_bool flag = ATOMIC_VAR_INIT(false);

void toggle_flag(void)
{
    flag ^= 1;
}

bool get_flag(void)
{
    return flag;
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when C atomic types or C++ std::atomic class variables appear twice in an expression and there are:

  • Two atomic read operations on the variable.

  • An atomic read and a distinct atomic write operation on the variable.

The C standard defines certain operations on atomic variables that are thread safe and do not cause data race conditions. Unlike individual operations, a pair of operations on the same atomic variable in an expression is not thread safe.

Risk

A thread can modify the atomic variable between the pair of atomic operations, which can result in a data race condition.

Fix

Do not reference an atomic variable twice in the same expression.

Example — Referencing Atomic Variable Twice in an Expression

#include <stdatomic.h>

atomic_int n = ATOMIC_VAR_INIT(0);

int compute_sum(void)
{
    return n * (n + 1) / 2;  //Noncompliant
}

In this example, the global variable n is referenced twice in the return statement of compute_sum(). The value of n can change between the two distinct read operations. compute_sum() can return an incorrect value.

Correction — Pass Variable as Function Argument

One possible correction is to pass the variable as a function argument n. The variable is copied to memory and the read operations on the copy guarantee that compute_sum() returns a correct result. If you pass a variable of type int instead of type atomic_int, the correction is still valid.


#include <stdatomic.h>

int compute_sum(atomic_int n)
{
    return n * (n + 1) / 2;
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when:

  1. Multiple tasks perform unprotected operations on a shared variable.

  2. At least one task performs a write operation.

  3. At least one operation is nonatomic. To detect data race on both atomic and nonatomic operations, use the options -detect-atomic-data-race. See Extend Data Race Checkers to Atomic Operations.

    See Define Atomic Operations in Multitasking Code.

If you activate this checker without specifying the multitasking options first, you see a warning in the log:

Warning: Checker 'Data Race' is activated but no protection have been defined
To find this defect, specify the multitasking options before analysis.

To specify these options, on the Configuration pane, select Multitasking. For more information, see Configuring Polyspace Multitasking Analysis Manually.

Risk

Data race can result in unpredictable values of the shared variable because you do not control the order of the operations in different tasks.

Data races between two write operations are more serious than data races between a write and read operation. Two write operations can interfere with each other and result in indeterminate values. To identify write-write conflicts, use the filters on the Detail column of the Results List pane. For these conflicts, the Detail column shows the additional line:

 Variable value may be altered by write-write concurrent access.
See also Filter and Group Results in Polyspace Desktop User Interface or Filter and Sort Results in Polyspace Access Web Interface (Polyspace Access).

Fix

To fix this defect, protect the operations on the shared variable using critical sections, temporal exclusion or another means. See Protections for Shared Variables in Multitasking Code.

To identify existing protections that you can reuse, see the table and graphs associated with the result. The table shows each pair of conflicting calls. The Access Protections column shows existing protections on the calls. To see the function call sequence leading to the conflicts, click the icon. For an example, see below.

Extend Checker

Extend this checker to check for data races in operations that Bug Finder might not detect by default. For instance:

Example — Unprotected Operation on Global Variable from Multiple Tasks


int var;  //Noncompliant
void begin_critical_section(void);
void end_critical_section(void);

void increment(void) {
    var++; 
}

void task1(void)  { 
      increment();
}

void task2(void)  { 
      increment();
}

void task3(void)  { 
     begin_critical_section();
     increment();
     end_critical_section();
}

In this example, to emulate multitasking behavior, specify the following options:

OptionSpecification
Configure multitasking manually
Tasks (-entry-points)

task1

task2

task3

Critical section details (-critical-section-begin -critical-section-end)Starting routineEnding routine
begin_critical_sectionend_critical_section

On the command-line, you can use the following:

 polyspace-bug-finder 
   -entry-points task1,task2,task3
   -critical-section-begin begin_critical_section:cs1
   -critical-section-end end_critical_section:cs1

In this example, the tasks task1, task2, and task3 call the function increment. increment contains the operation var++ that can involve multiple machine instructions including:

  • Reading var.

  • Writing an increased value to var.

These machine instructions, when executed from task1 and task2, can occur concurrently in an unpredictable sequence. For example, reading var from task1 can occur either before or after writing to var from task2. Therefore the value of var can be unpredictable.

Though task3 calls increment inside a critical section, other tasks do not use the same critical section. The operations in the critical section of task3 are not mutually exclusive with operations in other tasks.

Therefore, the three tasks are operating on a shared variable without common protection. In your result details, you see each pair of conflicting function calls.

If you click the icon, you see the function call sequence starting from the entry point to the read or write operation. You also see that the operation starting from task3 is in a critical section. The Access Protections entry shows the lock and unlock function that begin and end the critical section. In this example, you see the functions begin_critical_section and end_critical_section.

Correction — Place Operation in Critical Section

One possible correction is to place the operation in critical section. You can implement the critical section in multiple ways. For instance:

  • You can place var++ in a critical section. When task1 enters its critical section, the other tasks cannot enter their critical sections until task1 leaves its critical section. The operation var++ from the three tasks cannot interfere with each other.

    To implement the critical section, in the function increment, place the operation var++ between calls to begin_critical_section and end_critical_section.

    
    
    int var;
    
    void begin_critical_section(void);
    void end_critical_section(void);
    
    void increment(void) {
          begin_critical_section();
          var++;
          end_critical_section(); 
    }
    
    void task1(void)  { 
          increment();
    }
    
    void task2(void)  { 
          increment();
    }
    
    void task3(void)  { 
          increment();
    }
    

  • You can place the call to increment in the same critical section in the three tasks. When task1 enters its critical section, the other tasks cannot enter their critical sections until task1 leaves its critical section. The calls to increment from the three tasks cannot interfere with each other.

    To implement the critical section, in each of the three tasks, call increment between calls to begin_critical_section and end_critical_section.

    
    
    int var;
    
    void begin_critical_section(void);
    void end_critical_section(void);
    
    void increment(void) {
          var++;       
    }
    
    void task1(void)  { 
         begin_critical_section();
         increment();
         end_critical_section();
    }
    
    void task2(void)  { 
         begin_critical_section();
         increment();
         end_critical_section();
    }
    
    void task3(void)  { 
         begin_critical_section();
         increment();
         end_critical_section();
    }

Correction — Make Tasks Temporally Exclusive

Another possible correction is to make the tasks, task1, task2 and task3, temporally exclusive. Temporally exclusive tasks cannot execute concurrently.

On the Configuration pane, specify the following additional options:

On the command-line, you can use the following:

 polyspace-bug-finder 
     -temporal-exclusions-file "C:\exclusions_file.txt"
where the file C:\exclusions_file.txt has the following line:
task1 task2 task3

Example — Unprotected Operation in Threads Created with pthread_create
#include <pthread.h>

pthread_mutex_t count_mutex;
long long count; //Noncompliant


void* increment_count(void* args)
{
    count = count + 1;
    return NULL;
}

void* set_count(void *args)
{
    long long c;
    c = count;
    return NULL;
}

int main(void)
{
    pthread_t thread_increment;
    pthread_t thread_get;

    pthread_create(&thread_increment, NULL, increment_count, NULL);
    pthread_create(&thread_get, NULL, set_count, NULL);
    
    pthread_join(thread_get, NULL);
    pthread_join(thread_increment, NULL);

    return 1;
}

In this example, Bug Finder detects the creation of separate threads with pthread_create. The Data race defect is raised because the operation count = count + 1 in the thread with id thread_increment conflicts with the operation c = count in the thread with id thread_get. The variable count is accessed in multiple threads without a common protection.

The two conflicting operations are nonatomic. The operation c = count is nonatomic on 32-bit targets. See Define Atomic Operations in Multitasking Code.

Correction — Protect Operations with pthread_mutex_lock and pthread_mutex_unlock Pair

To prevent concurrent access on the variable count, protect operations on count with a critical section. Use the functions pthread_mutex_lock and pthread_mutex_unlock to implement the critical section.

#include <pthread.h>

pthread_mutex_t count_mutex;
long long count;


void* increment_count(void* args)
{
    pthread_mutex_lock(&count_mutex);
    count = count + 1;
    pthread_mutex_unlock(&count_mutex);
    return NULL;        
}

void* set_count(void *args)
{
    long long c;
    pthread_mutex_lock(&count_mutex);
    c = count;
    pthread_mutex_unlock(&count_mutex);
    return NULL;
}

int main(void)
{
    pthread_t thread_increment;
    pthread_t thread_get;

    pthread_create(&thread_increment, NULL, increment_count, NULL);
    pthread_create(&thread_get, NULL, set_count, NULL);

    pthread_join(thread_get, NULL);
    pthread_join(thread_increment, NULL);

    return 1;
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when:

  1. Multiple tasks perform unprotected operations on bit fields that are part of the same structure.

    For instance, a task operates on field errorFlag1 and another task on field errorFlag2 in a variable of this type:

    struct errorFlags {
       unsigned int errorFlag1 : 1;
       unsigned int errorFlag2 : 1;
       ...
    }
    Suppose that the operations are not atomic with respect to each other. In other words, you have not implemented protection mechanisms to ensure that one operation is completed before another operation begins.

  2. At least one of the unprotected operations is a write operation.

To find this defect, before analysis, you must specify the multitasking options. To specify these options, on the Configuration pane, select Multitasking. For more information, see Configuring Polyspace Multitasking Analysis Manually.

Risk

Adjacent bit fields that are part of the same structure might be stored in one byte in the same memory location. Read or write operations on all variables including bit fields occur one byte or word at a time. To modify only specific bits in a byte, steps similar to these steps occur in sequence:

  1. The byte is loaded into RAM.

  2. A mask is created so that only specific bits are modified to the intended value and the remaining bits remain unchanged.

  3. A bitwise OR operation is performed between the copy of the byte in RAM and the mask.

  4. The byte with specific bits modified is copied back from RAM.

When you access two different bit fields, these four steps have to be performed for each bit field. If the accesses are not protected, all four steps for one bit field might not be completed before the four steps for the other bit field begin. As a result, the modification of one bit field might undo the modification of an adjacent bit field. For instance, in the preceding example, the modification of errorFlag1 and errorFlag2 can occur in the following sequence. Steps 1,2 and 5 relate to modification of errorFlag1 and while steps 3,4 and 6 relate to that of errorFlag2.

  1. The byte with both errorFlag1 and errorFlag2 unmodified is copied into RAM, for purposes of modifying errorFlag1.

  2. A mask that modifies only errorFlag1 is bitwise OR-ed with this copy.

  3. The byte containing both errorFlag1 and errorFlag2 unmodified is copied into RAM a second time, for purposes of modifying errorFlag2.

  4. A mask that modifies only errorFlag2 is bitwise OR-ed with this second copy.

  5. The version with errorFlag1 modified is copied back. This version has errorFlag2 unmodified.

  6. The version with errorFlag2 modified is copied back. This version has errorFlag1 unmodified and overwrites the previous modification.

Fix

To fix this defect, protect the operations on bit fields that are part of the same structure by using critical sections, temporal exclusion, or another means. See Protections for Shared Variables in Multitasking Code.

To identify existing protections that you can reuse, see the table and graphs associated with the result. The table shows each pair of conflicting calls. The Access Protections column shows existing protections on the calls. To see the function call sequence leading to the conflicts, click the icon.

Example — Unprotected Operation on Global Variable from Multiple Tasks
typedef struct
{
   unsigned int IOFlag :1;
   unsigned int InterruptFlag :1;
   unsigned int Register1Flag :1;
   unsigned int SignFlag :1;
   unsigned int SetupFlag :1;
   unsigned int Register2Flag :1;
   unsigned int ProcessorFlag :1;
   unsigned int GeneralFlag :1;
} InterruptConfigbits_t;

InterruptConfigbits_t InterruptConfigbitsProc12;  //Noncompliant

void task1 (void) {
    InterruptConfigbitsProc12.IOFlag = 0;
}

void task2 (void) {
    InterruptConfigbitsProc12.SetupFlag = 0;
}

In this example, task1 and task2 access different bit fields IOFlag and SetupFlag, which belong to the same structured variable InterruptConfigbitsProc12.

To emulate multitasking behavior, specify the options listed in this table.

OptionSpecification
Configure multitasking manually
Tasks

task1

task2

At the command-line, use:

 polyspace-bug-finder 
   -entry-points task1,task2

Correction – Use Critical Sections

One possible correction is to wrap the bit field access in a critical section. A critical section lies between a call to a lock function and an unlock function. In this correction, the critical section lies between the calls to functions begin_critical_section and end_critical_section.

typedef struct
{
   unsigned int IOFlag :1;
   unsigned int InterruptFlag :1;
   unsigned int Register1Flag :1;
   unsigned int SignFlag :1;
   unsigned int SetupFlag :1;
   unsigned int Register2Flag :1;
   unsigned int ProcessorFlag :1;
   unsigned int GeneralFlag :1;
} InterruptConfigbits_t;

InterruptConfigbits_t InterruptConfigbitsProc12;

void begin_critical_section(void);
void end_critical_section(void);

void task1 (void) {
    begin_critical_section();
    InterruptConfigbitsProc12.IOFlag = 0;
    end_critical_section();
}

void task2 (void) {
    begin_critical_section();
    InterruptConfigbitsProc12.SetupFlag = 0;
    end_critical_section();
}

In this example, to emulate multitasking behavior, specify options listed in this table.

OptionSpecification
Configure multitasking manually
Tasks

task1

task2

Critical section detailsStarting routineEnding routine
begin_critical_sectionend_critical_section

At the command-line, use:

 polyspace-bug-finder 
   -entry-points task1,task2
   -critical-section-begin begin_critical_section:cs1
   -critical-section-end end_critical_section:cs1

Correction – Avoid Bit Fields

If you do not have memory constraints, use the char data type instead of bit fields. The char variables in a structure occupy at least one byte and do not have the thread-safety issues that come from bit manipulations in a byte-sized operation. Data races do not result from unprotected operations on different char variables that are part of the same structure.

typedef struct
{
   unsigned char IOFlag;
   unsigned char InterruptFlag;
   unsigned char Register1Flag;
   unsigned char SignFlag;
   unsigned char SetupFlag;
   unsigned char Register2Flag;
   unsigned char ProcessorFlag;
   unsigned char GeneralFlag;
} InterruptConfigbits_t;

InterruptConfigbits_t InterruptConfigbitsProc12;

void task1 (void) {
    InterruptConfigbitsProc12.IOFlag = 0;
}

void task2 (void) {
    InterruptConfigbitsProc12.SetupFlag = 0;
}

Though the checker does not flag this correction, do not use this correction for C99 or earlier. Only from C11 and later does the C Standard mandate that distinct char variables cannot be accessed using the same word.

Correction – Insert Bit Field of Size 0

You can enter a non-bit field member or an unnamed bit field member of size 0 between two adjacent bit fields that might be accessed concurrently. A non-bit field member or size 0 bit field member ensures that the subsequent bit field starts from a new memory location. In this corrected example, the size 0 bit field member ensures that IOFlag and SetupFlag are stored in distinct memory locations.

typedef struct
{
   unsigned int IOFlag :1;
   unsigned int InterruptFlag :1;
   unsigned int Register1Flag :1;
   unsigned int SignFlag :1;
   unsigned int : 0;
   unsigned int SetupFlag :1;
   unsigned int Register2Flag :1;
   unsigned int ProcessorFlag :1;
   unsigned int GeneralFlag :1;
} InterruptConfigbits_t;

InterruptConfigbits_t InterruptConfigbitsProc12;

void task1 (void) {
    InterruptConfigbitsProc12.IOFlag = 0;
}

void task2 (void) {
    InterruptConfigbitsProc12.SetupFlag = 0;
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when:

  1. Multiple tasks call the same standard library function.

    For instance, multiple tasks call the strerror function.

  2. The calls are not protected using a common protection.

    For instance, the calls are not protected by the same critical section.

Functions flagged by this defect are not guaranteed to be reentrant. A function is reentrant if it can be interrupted and safely called again before its previous invocation completes execution. If a function is not reentrant, multiple tasks calling the function without protection can cause concurrency issues. For the list of functions that are flagged, see CON33-C: Avoid race conditions when using library functions.

To find this defect, you must specify the multitasking options before analysis. To specify these options, on the Configuration pane, select Multitasking. For more information, see Configuring Polyspace Multitasking Analysis Manually.

Risk

The functions flagged by this defect are nonreentrant because their implementations can use global or static variables. When multiple tasks call the function without protection, the function call from one task can interfere with the call from another task. The two invocations of the function can concurrently access the global or static variables and cause unpredictable results.

The calls can also cause more serious security vulnerabilities, such as abnormal termination, denial-of-service attack, and data integrity violations.

Fix

To fix this defect, do one of the following:

  • Use a reentrant version of the standard library function if it exists.

    For instance, instead of strerror(), use strerror_r() or strerror_s(). For alternatives to functions flagged by this defect, see the documentation for CON33-C.

  • Protect the function calls using common critical sections or temporal exclusion.

    See Critical section details (-critical-section-begin -critical-section-end) and Temporally exclusive tasks (-temporal-exclusions-file).

    To identify existing protections that you can reuse, see the table and graphs associated with the result. The table shows each pair of conflicting calls. The Access Protections column shows existing protections on the calls. To see the function call sequence leading to the conflicts, click the icon. For an example, see below.

Example — Unprotected Call to Standard Library Function from Multiple Tasks
#include <errno.h>
#include <stdio.h>
#include <string.h>

void begin_critical_section(void);
void end_critical_section(void);

FILE *getFilePointer(void);

void func(FILE *fp) {
  fpos_t pos;
  errno = 0;
  if (0 != fgetpos(fp, &pos)) {
    char *errmsg = strerror(errno);  //Noncompliant
    printf("Could not get the file position: %s\n", errmsg);
  }
}

void task1(void) {
    FILE* fptr1 = getFilePointer();
    func(fptr1);
}

void task2(void) {
     FILE* fptr2 = getFilePointer();
     func(fptr2);
}

void task3(void) {
     FILE* fptr3 = getFilePointer();
     begin_critical_section();
     func(fptr3);
     end_critical_section();
}

In this example, to emulate multitasking behavior, specify the following options:

OptionSpecification
Configure multitasking manually
Tasks (-entry-points)

task1

task2

task3

Critical section details (-critical-section-begin -critical-section-end)Starting routineEnding routine
begin_critical_sectionend_critical_section

On the command-line, you can use the following:

 polyspace-bug-finder
   -entry-points task1,task2,task3
   -critical-section-begin begin_critical_section:cs1
   -critical-section-end end_critical_section:cs1

In this example, the tasks, task1, task2 and task3, call the function func. func calls the nonreentrant standard library function, strerror.

Though task3 calls func inside a critical section, other tasks do not use the same critical section. Operations in the critical section of task3 are not mutually exclusive with operations in other tasks.

These three tasks are calling a nonreentrant standard library function without common protection. In your result details, you see each pair of conflicting function calls.

If you click the icon, you see the function call sequence starting from the entry point to the standard library function call. You also see that the call starting from task3 is in a critical section. The Access Protections entry shows the lock and unlock function that begin and end the critical section. In this example, you see the functions begin_critical_section and end_critical_section.

Correction — Use Reentrant Version of Standard Library Function

One possible correction is to use a reentrant version of the standard library function strerror. You can use the POSIX version strerror_r which has the same functionality but also guarantees thread-safety.

#include <errno.h>
#include <stdio.h>
#include <string.h>

void begin_critical_section(void);
void end_critical_section(void);

FILE *getFilePointer(void);
enum { BUFFERSIZE = 64 };

void func(FILE *fp) {
  fpos_t pos;
  errno = 0;
  if (0 != fgetpos(fp, &pos)) {
    char errmsg[BUFFERSIZE];
    if (strerror_r(errno, errmsg, BUFFERSIZE) != 0) {
      /* Handle error */
    }
    printf("Could not get the file position: %s\n", errmsg);
  }
}

void task1(void) {
    FILE* fptr1 = getFilePointer();
    func(fptr1);
}

void task2(void) {
     FILE* fptr2 = getFilePointer();
     func(fptr2);
}

void task3(void) {
     FILE* fptr3 = getFilePointer();
     begin_critical_section();
     func(fptr3);
     end_critical_section();
}
Correction — Place Function Call in Critical Section

One possible correction is to place the call to strerror in critical section. You can implement the critical section in multiple ways.

For instance, you can place the call to the intermediate function func in the same critical section in the three tasks. When task1 enters its critical section, the other tasks cannot enter their critical sections until task1 leaves its critical section. The calls to func and therefore the calls to strerror from the three tasks cannot interfere with each other.

To implement the critical section, in each of the three tasks, call func between calls to begin_critical_section and end_critical_section.

#include <errno.h>
#include <stdio.h>
#include <string.h>

void begin_critical_section(void);
void end_critical_section(void);

FILE *getFilePointer(void);

void func(FILE *fp) {
  fpos_t pos;
  errno = 0;
  if (0 != fgetpos(fp, &pos)) {
    char *errmsg = strerror(errno);
    printf("Could not get the file position: %s\n", errmsg);
  }
}

void task1(void) {
    FILE* fptr1 = getFilePointer();
    begin_critical_section();
    func(fptr1);
    end_critical_section();
}

void task2(void) {
     FILE* fptr2 = getFilePointer();
     begin_critical_section();
     func(fptr2);
     end_critical_section();
}

void task3(void) {
     FILE* fptr3 = getFilePointer();
     begin_critical_section();
     func(fptr3);
     end_critical_section();
}

Correction — Make Tasks Temporally Exclusive

Another possible correction is to make the tasks, task1, task2 and task3, temporally exclusive. Temporally exclusive tasks cannot execute concurrently.

On the Configuration pane, specify the following additional options:

On the command-line, you can use the following:

 polyspace-bug-finder
     -temporal-exclusions-file "C:\exclusions_file.txt"
where the file C:\exclusions_file.txt has the following line:
task1 task2 task3

Check Information

Category: Concurrency Issues

Version History

Introduced in R2023a