【Linux】线程的控制

news2024/11/15 11:15:40

目录

POSIX线程库

常用的POSIX线程库接口声明:

注意事项

创建一个进程

pthread_create函数

参数

返回值

使用示例

线程ID和进程地址空间布局

线程ID

进程地址空间布局

示例图

获取一个进程的线程ID

函数原型

返回值

使用示例

注意事项

线程终止

pthread_cancel函数

函数原型

参数

返回值

取消状态和类型

示例代码

pthread_exit函数

使用pthread_exit的代码示例

注意事项

线程等待

函数原型

参数

返回值

使用场景

注意事项

示例代码

分离线程

分离线程具有以下几个关键特性和优势:

pthread_detach函数

pthread_detach函数原型

参数

返回值

注意事项

示范代码

线程的互斥

互斥量的相关接口

1. 互斥量的相关接口

1.1 初始化互斥量

1.2 销毁互斥量

1.3 加锁(获取互斥量)

1.4 尝试加锁(非阻塞获取互斥量)

1.5 解锁(释放互斥量)

2. 示范代码

线程安全和可重入

线程安全

可重入函数

常见锁的概念

1. 互斥锁(Mutex)

2. 读写锁(Read-Write Lock)

3. 自旋锁(Spinlock)

4. 条件变量(Condition Variable)

死锁


POSIX线程库

Linux的POSIX线程库(pthread库)是用于支持线程的创建和管理的库。线程是操作系统能够并发执行的一个基本单元,它是进程的一个执行实例。在C/C++中,通过pthread库可以方便地创建、管理线程,以及实现线程间的同步和通信。

常用的POSIX线程库接口声明:

  1. 线程创建与退出

    • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);:用于创建一个新线程。

    • void pthread_exit(void *retval);:用于终止调用线程,并返回一个指向某个对象的指针。

  2. 线程等待与同步

    • int pthread_join(pthread_t thread, void **retval);:用于阻塞当前线程,直到指定的thread线程终止。

    • int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);:用于锁定和解锁互斥量,实现线程间的同步。

  3. 线程属性与标识

    • pthread_t pthread_self(void);:用于获取调用线程的线程标识符。

    • int pthread_detach(pthread_t thread);:用于将线程分离,使得线程在终止时自动释放其所有资源。

  4. 线程取消与比较

    • int pthread_cancel(pthread_t thread);:用于请求取消指定的线程。

    • int pthread_equal(pthread_t t1, pthread_t t2);:用于比较两个线程标识符是否相等。

注意事项

当pthreads函数出错时,它们通常不会设置全局变量errno,而是将错误代码通过返回值返回。同时,pthreads也提供了线程内的errno变量,以支持其他使用errno的代码。在处理pthreads函数的错误时,建议通过返回值判定,因为读取返回值通常比读取线程内的errno变量开销更小。

创建一个进程

pthread_create函数

pthread_create 是 POSIX 线程(pthread)库中的一个重要函数,它用于在程序中创建一个新的线程。这个函数的原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                    void *(*start_routine) (void *), void *arg);

下面是关于这个函数的各个参数和返回值的详细解释:

参数

  1. **pthread_t *thread**:

    • 这是一个指向 pthread_t 类型变量的指针,用于存储新创建线程的标识符。当 pthread_create 成功创建一个线程后,新线程的线程ID会被存储在这个指针所指向的位置。

  2. **const pthread_attr_t *attr**:

    • 这是一个指向 pthread_attr_t 类型变量的指针,用于指定线程的属性。这个参数可以为 NULL,表示使用默认的线程属性。线程属性可以包括线程栈的大小、调度策略、优先级等。

  3. **void *(*start_routine) (void *)**:

    • 这是一个函数指针,指向新线程开始执行时调用的函数(即线程任务函数)。这个函数接受一个 void* 类型的参数,并返回一个 void* 类型的值。通常,线程任务函数会执行线程需要完成的工作。

  4. **void *arg**:

    • 这是一个指向任意类型的指针,作为参数传递给线程任务函数。这个参数可以为 NULL,表示不传递任何参数给线程任务函数。

返回值

  • pthread_create 函数成功时返回0,失败时返回错误码。这些错误码在 <errno.h> 头文件中定义,并可以通过 perrorstrerror 函数转换为人类可读的字符串。

使用示例

下面是一个简单的 pthread_create 使用示例:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

// 线程任务函数
void *threadTask(void *arg) {
    // 在这里执行线程的工作
    printf("I am a thread running the task function!\n");
    return NULL;
}

int main() {
    pthread_t tid; // 线程ID
    int ret; // 存储pthread_create的返回值

    // 创建线程
    ret = pthread_create(&tid, NULL, threadTask, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %s\n", strerror(ret));
        return EXIT_FAILURE;
    }

    // 等待线程结束(在实际程序中,这里通常会有其他逻辑)
    pthread_join(tid, NULL);

    printf("Main thread continues after thread creation.\n");
    return EXIT_SUCCESS;
}

在这个例子中,threadTask 是线程任务函数,它会被新创建的线程执行。main 函数中调用 pthread_create 来创建这个线程,并检查返回值以确保线程被成功创建。然后,主线程通过 pthread_join 等待新线程结束。注意,在实际的多线程程序中,主线程通常会继续执行其他任务,而不是简单地等待新线程结束。

pthread_create 是 POSIX 线程库中创建线程的基本方式,它提供了在单个进程内并发执行多个线程的能力,从而提高了程序的并行性和效率。

线程ID和进程地址空间布局

线程ID和进程地址空间布局是操作系统中与多线程和多进程相关的核心概念。下面我会详细解释这两个概念,并尝试给出示例图。

线程ID

线程ID(Thread ID)是一个唯一标识符,用于区分同一进程内的不同线程。每个线程在创建时都会被分配一个唯一的线程ID,这个ID在进程的生命周期内保持不变,并且对于操作系统来说,它是区分不同线程的关键。

线程ID通常是一个整数,其大小和表示方式取决于具体的操作系统和编程环境。在POSIX线程库中,线程ID是通过pthread_t类型来表示的。

进程地址空间布局

进程地址空间是指一个进程在虚拟内存中的映射。每个进程都有自己独立的地址空间,这使得不同进程之间的数据是隔离的,从而提高了系统的安全性和稳定性。

进程地址空间通常包括以下几个部分:

  1. 文本段(Text Segment):也称为代码段,存储程序的二进制代码,即CPU执行的机器指令。这部分是只读的,防止程序意外地修改了它的指令。

  2. 数据段(Data Segment):存储程序中已初始化的全局变量和静态变量。这部分在程序加载时会被初始化,并且在程序执行期间可以修改。

  3. BSS段:存储程序中未初始化的全局变量和静态变量。BSS段在程序加载时不占用实际的磁盘空间,只占用内存空间。

  4. 堆(Heap):动态内存分配的区域,由程序员在运行时通过如mallocnew等函数进行分配和释放。

  5. 栈(Stack):用于存储局部变量和函数调用的信息。每个线程都有自己的栈,用于保存线程的执行上下文(如函数调用的参数、返回地址等)。

在图形表示中,进程地址空间通常是一个从低地址到高地址的连续区域,各个段按照上述顺序排列。

示例图

下面是一个简化的进程地址空间布局的示例图:

获取一个进程的线程ID

pthread_self 是 POSIX 线程库中的一个函数,用于获取当前执行线程的线程标识符(Thread ID)。这个函数对于识别和管理线程特别有用,尤其是在多线程环境中。

函数原型

pthread_t pthread_self(void);

返回值

pthread_self 函数返回一个 pthread_t 类型的值,这个值表示调用它的线程的线程ID。pthread_t 是一个不透明的数据类型,通常用于在POSIX线程库中唯一地标识一个线程。尽管其具体实现可能因系统和库的不同而有所差异,但 pthread_t 总是被设计用来在系统中唯一地标识一个线程。

使用示例

以下是一个简单的示例,展示了如何使用 pthread_self 函数来获取当前线程的线程ID,并将其打印出来:

#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg) {
    pthread_t thread_id = pthread_self();
    printf("Thread ID in thread function: %lu\n", (unsigned long)thread_id);
    return NULL;
}

int main() {
    pthread_t thread;
    int ret;

    // 创建线程
    ret = pthread_create(&thread, NULL, thread_function, NULL);
    if (ret != 0) {
        fprintf(stderr, "Error creating thread: %s\n", strerror(ret));
        return 1;
    }

    // 在主线程中获取并打印线程ID
    pthread_t main_thread_id = pthread_self();
    printf("Main thread ID: %lu\n", (unsigned long)main_thread_id);

    // 等待线程结束
    pthread_join(thread, NULL);

    return 0;
}

在这个例子中,我们创建了一个新线程来执行 thread_function 函数。在这个函数内部,我们使用 pthread_self 获取当前线程的ID,并打印出来。同时,在 main 函数中,我们也调用 pthread_self 来获取主线程的线程ID,并打印出来。通过比较这两个线程ID,你可以看到它们是不同的,从而验证了 pthread_self 能够正确返回当前线程的ID。

注意事项

  • pthread_self 函数是线程安全的,可以在多线程环境中安全地调用。

  • 线程ID在进程的生命周期内是唯一的,但在不同的进程之间可能不是唯一的。

  • 线程ID主要用于线程间通信、线程管理和调试目的。

通过 pthread_self 函数,程序员可以方便地获取当前线程的线程ID,从而能够更精确地控制和追踪线程的行为。这在多线程编程中是非常有用的,特别是在需要进行线程同步、互斥操作或者调试多线程程序时。

线程终止

在Linux中,线程的终止通常与进程的终止类似,但也有一些特殊之处,尤其是当我们讨论POSIX线程(pthreads)时。线程的终止可以通过几种方式实现,包括正常退出、取消线程、因接收信号而终止以及由于某些错误条件而终止。

pthread_exit是POSIX线程库中用于线程正常退出的函数。当一个线程调用pthread_exit时,它将停止执行并释放所有它占用的系统资源。然而,线程的标识符(thread ID)和其属性并不会立即被删除,直到其他线程对它调用了pthread_join

pthread_cancel 是 POSIX 线程(pthreads)库中的一个函数,用于向指定的线程发送取消请求。当线程收到取消请求时,如果它允许取消,则会在某个取消点(cancellation point)上终止执行。这提供了一种机制,允许一个线程优雅地请求另一个线程的终止。

pthread_cancel函数

函数原型

int pthread_cancel(pthread_t thread);

参数

  • thread:要取消的线程的线程标识符(pthread_t 类型)。

返回值

如果成功,返回 0;如果出错,返回错误码。

取消状态和类型

线程的取消状态可以通过 pthread_setcancelstate 来设置,它可以是 PTHREAD_CANCEL_ENABLE(允许取消)或 PTHREAD_CANCEL_DISABLE(禁止取消)。此外,线程的取消类型可以通过 pthread_setcanceltype 来设置,它可以是 PTHREAD_CANCEL_ASYNCHRONOUS(异步取消,即收到取消请求时立即取消)或 PTHREAD_CANCEL_DEFERRED(延迟取消,即直到线程到达某个取消点时才取消)。

示例代码

下面是一个简单的示例,演示了如何使用 pthread_cancel 来取消一个线程:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数,将被取消的线程将执行这个函数
void *thread_function(void *arg) {
    printf("Thread started, ID: %lu\n", (unsigned long)pthread_self());

    // 设置取消状态为可取消,并设置取消类型为延迟取消
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);

    // 模拟一些工作
    for (int i = 0; i < 10; ++i) {
        printf("Thread working...\n");
        sleep(1); // 休眠一秒,模拟耗时操作
    }

    printf("Thread exiting normally\n");
    return NULL;
}

int main() {
    pthread_t thread_id;
    int ret;

    // 创建线程
    ret = pthread_create(&thread_id, NULL, thread_function, NULL);
    if (ret != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 让主线程休眠几秒,以便让新创建的线程有时间开始执行
    sleep(2);

    // 发送取消请求给线程
    printf("Sending cancel request to thread\n");
    ret = pthread_cancel(thread_id);
    if (ret != 0) {
        perror("pthread_cancel");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束
    void *thread_result;
    ret = pthread_join(thread_id, &thread_result);
    if (ret != 0) {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    if (thread_result == PTHREAD_CANCELED) {
        printf("Thread was canceled\n");
    } else {
        printf("Thread exited with status\n");
    }

    exit(EXIT_SUCCESS);
}

在上面的代码中,我们创建了一个新线程,并让它执行 thread_function 函数。主线程在创建新线程后休眠两秒,然后向新线程发送取消请求。新线程在开始时设置了取消状态和取消类型,并在循环中模拟一些工作。如果线程收到取消请求,并且它的取消类型设置为 PTHREAD_CANCEL_DEFERRED,则它将在循环中的某个点(即取消点,通常是系统调用或其他库函数)上被取消。在这个例子中,循环中的 sleep 函数就是一个取消点。

注意,不是所有的库函数都是取消点。有些函数,如 pthread_cancelpthread_setcancelstatepthread_setcanceltypepthread_testcancel 是线程取消的例外,它们在执行时不会响应取消请求。

最后,主线程使用 pthread_join 等待被取消的线程结束,并检查线程的退出状态。如果被取消,pthread_join 的第二个参数将设置为 PTHREAD_CANCELED

pthread_exit函数

pthread_exit函数的原型如下:

void pthread_exit(void *retval);

这里,retval是一个指向某个类型的指针,它允许线程返回一个指向某种类型数据的指针,这个返回值可以通过pthread_join函数来获取。如果线程没有返回值,或者不需要返回值,可以传递NULLpthread_exit

使用pthread_exit的代码示例

下面是一个简单的示例,演示了如何使用pthread_exit来终止一个线程,并使用pthread_join来获取线程的返回值:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数
void *thread_function(void *arg) {
    // 线程执行的代码
    printf("Thread is running...\n");
    
    // 线程执行完成后,使用pthread_exit退出
    pthread_exit((void *)1); // 传递一个整数值作为退出状态
}

int main() {
    pthread_t thread_id;
    void *thread_result;
    int ret;

    // 创建线程
    ret = pthread_create(&thread_id, NULL, thread_function, NULL);
    if (ret != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束,并获取线程的返回值
    ret = pthread_join(thread_id, &thread_result);
    if (ret != 0) {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    // 打印线程的返回值
    printf("Thread exited with status %ld\n", (long)thread_result);

    exit(EXIT_SUCCESS);
}

在这个例子中,thread_function是一个线程函数,它执行一些工作,然后调用pthread_exit来退出线程,并传递一个整数值作为退出状态。主线程通过调用pthread_join等待thread_function线程结束,并获取其返回值。

注意事项

  • 线程一旦调用pthread_exit,就不能再重新启动或继续执行。

  • 线程结束时,其局部存储区会被释放,但全局变量、静态变量和动态分配的内存(如使用malloc分配的内存)不会立即释放。这些资源会在进程结束时被操作系统清理。

  • 线程可以通过调用pthread_cancel函数被其他线程取消。被取消的线程最终会收到一个取消请求,并且如果它允许取消,则会调用pthread_cleanup_pushpthread_cleanup_pop注册的清理函数,并最终调用pthread_exit退出。

  • 如果一个线程被信号终止(例如收到SIGKILL),它不会有机会执行清理函数或调用pthread_exit

线程等待

在Linux中,线程的等待主要涉及到pthread_join函数,该函数用于等待一个线程的结束。当一个线程调用pthread_join时,它会阻塞,直到指定的线程结束执行。这提供了同步机制,使得一个线程可以等待另一个线程完成其任务。

函数原型

int pthread_join(pthread_t thread, void **retval);

参数

  • thread:要等待的线程的线程标识符(pthread_t类型)。

  • retval:一个指向void指针的指针,用于存储被等待线程的返回值。如果不需要获取返回值,可以传递NULL

返回值

如果成功,函数返回0;如果出错,返回错误码。

使用场景

pthread_join通常用于以下场景:

  1. 收集线程的返回值:如果线程函数有返回值,并且主线程或其他线程需要这个返回值,那么可以使用pthread_join来获取它。

  2. 确保线程完成其任务:有时,主线程或其他线程需要等待某个线程完成其特定的任务或资源释放操作,以确保数据的一致性或避免资源竞争。

  3. 线程同步:在某些复杂的并发场景中,线程之间的同步是必需的。pthread_join提供了一种简单的方式来同步线程的执行顺序。

注意事项

  • 如果一个线程已经结束,并且另一个线程调用pthread_join来等待它,那么pthread_join会立即返回。

  • 如果一个线程被取消(通过pthread_cancel),并且另一个线程正在等待它(通过pthread_join),那么pthread_join将返回PTHREAD_CANCELED

  • 如果尝试对一个不可加入的线程(即设置了线程属性为PTHREAD_CREATE_DETACHED)调用pthread_join,将会导致错误。

  • pthread_join会阻塞调用线程,直到被等待的线程结束。这意味着如果主线程调用pthread_join等待一个子线程,那么主线程将不会继续执行,直到子线程结束。

示例代码

下面是一个简单的示例,演示了如何使用pthread_join来等待线程结束:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数
void *thread_function(void *arg) {
    printf("Thread is running...\n");
    // 模拟一些工作
    sleep(2);
    printf("Thread is exiting...\n");
    return (void *)0; // 返回值
}

int main() {
    pthread_t thread_id;
    void *thread_result;
    int ret;

    // 创建线程
    ret = pthread_create(&thread_id, NULL, thread_function, NULL);
    if (ret != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束
    printf("Main thread is waiting for the thread to finish...\n");
    ret = pthread_join(thread_id, &thread_result);
    if (ret != 0) {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    printf("Thread has finished, return value: %ld\n", (long)thread_result);

    exit(EXIT_SUCCESS);
}

在上面的代码中,我们创建了一个新线程,并使用pthread_join在主线程中等待它结束。pthread_join会阻塞主线程,直到线程函数执行完毕。一旦线程结束,pthread_join返回,并且主线程可以继续执行。

通过pthread_join,我们可以确保线程的正确同步和资源的有效管理,特别是在涉及线程间数据共享和依赖的场景中。

分离线程

在Linux中,线程的分离(Detached Thread)是一种特殊的线程设置,它决定了线程的生命周期是否与主线程的生命周期独立。当一个线程被设置为分离线程时,其资源在退出时会被自动回收,而不需要主线程等待其结束并调用线程清理函数。这种机制有助于简化线程管理,提高程序的性能和效率。

分离线程具有以下几个关键特性和优势:

  1. 资源自动回收:分离线程在退出时能够自动回收其占用的系统资源,包括内存和线程描述符等。这避免了资源泄漏和内存泄漏的风险,因为主线程无需显式地等待和回收分离线程的资源。

  2. 提高性能和效率:由于分离线程的自动回收机制,主线程无需等待分离线程结束,从而减少了主线程的等待时间。这使得主线程可以继续执行其他任务,而不会被阻塞在等待分离线程结束的操作上,从而提高了程序的性能和效率。

  3. 简化代码和逻辑:使用分离线程可以简化线程编程的代码和逻辑。程序员无需关注和处理线程的退出和资源回收,这使得编写简洁、清晰和可维护的多线程代码变得更加容易。

在Linux中,你可以使用pthread_detach函数来设置线程为分离状态。该函数的原型如下:

int pthread_detach(pthread_t thread);

其中,thread参数是你想要设置为分离状态的线程的标识符。如果函数成功执行,它将返回0;否则,将返回一个错误码。

需要注意的是,一旦线程被设置为分离状态,你就不能再使用pthread_join函数来等待它的结束。如果你尝试对一个已经分离的线程使用pthread_join,将会导致错误。

此外,还需要注意的是,默认情况下,新创建的线程是可连接的(joinable),这意味着你需要显式地调用pthread_join来等待线程的结束并回收其资源。如果你不关心线程的返回值,或者想要避免等待和回收资源的复杂性,那么使用分离线程可能是一个好选择。

总之,Linux中的分离线程是一种强大的机制,它可以帮助你更有效地管理线程资源,提高程序的性能和效率,并简化多线程编程的复杂性。 在Linux中,pthread_detach函数用于将线程设置为分离状态。当线程被设置为分离后,它不再需要其他线程来调用pthread_join来回收其资源。当分离线程结束时,其相关资源(如线程栈)会自动被系统回收。

pthread_detach函数

pthread_detach函数原型

int pthread_detach(pthread_t thread);

参数

  • thread:需要被设置为分离状态的线程的标识符。

返回值

如果成功,函数返回0;如果失败,返回错误码。

注意事项

  • 一旦线程被分离,你就不能再对它使用pthread_join函数。

  • 分离状态的线程在其终止时自动释放其所有资源。

  • 主线程(或其他线程)不需要等待分离线程结束,这有助于减少线程间的同步开销。

示范代码

下面是一个使用pthread_detach的简单示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 线程函数
void *detached_thread_function(void *arg) {
    printf("Detached thread started, ID: %lu\n", (unsigned long)pthread_self());
    // 模拟一些工作
    for (int i = 0; i < 5; ++i) {
        printf("Detached thread working...\n");
        sleep(1);
    }
    printf("Detached thread exiting...\n");
    pthread_exit(NULL); // 线程正常退出
}

int main() {
    pthread_t thread_id;
    int ret;

    // 创建线程
    ret = pthread_create(&thread_id, NULL, detached_thread_function, NULL);
    if (ret != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 将线程设置为分离状态
    ret = pthread_detach(thread_id);
    if (ret != 0) {
        perror("pthread_detach");
        exit(EXIT_FAILURE);
    }

    printf("Main thread: Detached the thread with ID %lu\n", (unsigned long)thread_id);

    // 主线程继续执行其他任务,不需要等待分离线程结束
    printf("Main thread continuing its work...\n");
    sleep(6); // 主线程休眠,以便观察分离线程的执行情况

    printf("Main thread exiting...\n");
    exit(EXIT_SUCCESS);
}

在这个示例中,我们创建了一个线程并立即将其设置为分离状态。主线程不需要调用pthread_join来等待这个线程结束,而是继续执行其他任务。分离线程会执行其任务并在完成后自动退出,其资源也会被自动回收。

运行这段代码,你将看到主线程和分离线程交替打印消息,但主线程不会因等待分离线程结束而被阻塞。分离线程在完成其任务后会自动退出,而主线程将继续执行并最终退出程序。

请注意,虽然这个示例很简单,但在实际应用中,线程管理和同步可能涉及更复杂的逻辑和考虑因素。务必谨慎处理线程之间的同步和资源共享,以避免出现竞态条件和死锁等问题。

线程的互斥

Linux中的线程互斥

一、互斥的概念与重要性

互斥是一种同步机制,用于确保在某一时刻只有一个线程可以访问共享的数据资源。在多线程编程中,互斥锁扮演着至关重要的角色,它有助于防止数据不一致和其他并发问题。

二、互斥锁的实现与工作原理

  1. 操作系统与硬件支持

    互斥锁的实现通常涉及到底层的操作系统支持和硬件支持。

  2. 锁的获取与释放

    当一个线程尝试获取互斥锁时,如果锁已被其他线程持有,则该线程将被阻塞。一旦线程获取了互斥锁,它就可以安全地访问共享资源,并在完成访问后释放锁。

三、互斥锁的使用注意事项

  1. 避免死锁

    死锁是指两个或更多线程在等待对方释放资源的情况,导致它们都无法继续执行。为了避免死锁,程序员需要仔细设计线程间的交互和同步机制。

  2. 防止饥饿

    饥饿是指某些线程可能长时间无法获取到锁,导致它们无法完成其任务。为了避免饥饿,需要确保锁的获取和释放操作公平且合理。

四、总结

Linux中的线程互斥是一种重要的同步机制,它确保了多线程环境下对共享资源的访问具有独占性和一致性。通过合理使用互斥锁和其他同步工具,程序员可以编写出高效且可靠的多线程程序。

互斥量的相关接口

Linux中的互斥量(mutex)是POSIX线程库(pthread)提供的一种同步原语,用于保护共享资源免受并发访问的影响。下面将详细介绍互斥量的相关接口,并给出示范代码。

1. 互斥量的相关接口

1.1 初始化互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • mutex:指向要初始化的互斥量的指针。

  • attr:用于指定互斥量属性的对象,通常设为NULL以使用默认属性。

1.2 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • mutex:指向要销毁的互斥量的指针。

1.3 加锁(获取互斥量)
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • mutex:指向要获取的互斥量的指针。

1.4 尝试加锁(非阻塞获取互斥量)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • mutex:指向要尝试获取的互斥量的指针。

1.5 解锁(释放互斥量)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex:指向要释放的互斥量的指针。

2. 示范代码

下面是一个简单的示例,展示如何使用互斥量来同步对共享资源的访问:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 共享资源
int shared_resource = 0;

// 互斥量
pthread_mutex_t mutex;

// 线程函数
void *increment_resource(void *arg) {
    long iterations = (long)arg;
    for (long i = 0; i < iterations; ++i) {
        // 加锁
        pthread_mutex_lock(&mutex);

        // 访问共享资源
        ++shared_resource;

        // 解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    // 初始化互斥量
    if (pthread_mutex_init(&mutex, NULL) != 0) {
        perror("pthread_mutex_init");
        exit(EXIT_FAILURE);
    }

    // 创建线程
    const int num_threads = 5;
    const long num_iterations = 1000000;
    pthread_t threads[num_threads];
    for (int i = 0; i < num_threads; ++i) {
        if (pthread_create(&threads[i], NULL, increment_resource, (void *)num_iterations) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < num_threads; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁互斥量
    if (pthread_mutex_destroy(&mutex) != 0) {
        perror("pthread_mutex_destroy");
        exit(EXIT_FAILURE);
    }

    // 打印最终结果
    printf("Final value of shared_resource: %d\n", shared_resource);

    return 0;
}

在这个例子中,我们有一个共享资源shared_resource,以及一个互斥量mutex用于保护这个资源。我们创建了5个线程,每个线程都会多次增加shared_resource的值。通过使用互斥量,我们可以确保每次只有一个线程能够访问和修改shared_resource,从而避免了数据竞争和不一致的问题。

注意,在编写多线程程序时,还需要考虑其他同步机制(如条件变量、读写锁等)以及线程间的通信方式,以确保程序的正确性和性能。同时,也需要注意处理线程创建、同步和销毁时的错误情况,以避免程序崩溃或资源泄漏。

线程安全和可重入

线程安全

线程安全是多线程编程中的一个重要概念。在多线程运行的环境中,不论线程的调度顺序如何,如果最终的结果都是一致且正确的,那么这些线程就被认为是线程安全的。线程安全主要涉及到两个方面:

  1. 线程同步:确保同一时刻只有一个线程访问临界资源。这通常通过互斥锁、条件变量等同步机制来实现,以避免多个线程同时修改同一数据导致的数据不一致问题。

  2. 使用线程安全的函数:线程安全的函数指的是那些可以被多个线程同时调用而不会发生竞态条件的函数。竞态条件是指两个或多个线程在访问共享资源时,由于它们的执行顺序不同而导致结果不一致的情况。

造成线程不安全的原因通常是因为两个或更多的线程对象共享同一个资源,并且这些线程在没有适当同步的情况下操作这些共享资源。例如,全局变量在同一进程中的多个线程之间是共享的,如果多个线程在没有锁保护的情况下同时修改全局变量,就可能导致数据不一致。

可重入函数

可重入函数与线程安全有着密切的关系。一个函数被称为可重入的,如果它可以在任何时候被中断,然后在稍后的某个时刻从中断点继续执行,而不会丢失数据或产生不正确的结果。换句话说,可重入函数具有无状态性,即其执行不依赖于先前的函数调用或全局状态。

常见的不可重入的情况包括:

  1. 调用了malloc/free函数,因为malloc函数是使用全局链表来管理堆的,可能会被其他线程打断。

  2. 调用了标准I/O库函数,标准I/O库的很多实现都是以不可重入的方式使用的。

  3. 使用了全局变量或静态变量,并且这些变量的状态在函数执行过程中会发生变化。

如果一个函数是线程安全的,那么它通常也是可重入的,因为线程安全要求函数在多线程环境中能够正确运行,而不会导致数据竞争或其他并发问题。反之,如果一个函数是不可重入的,那么它就不能由多个线程同时使用,否则可能会引发线程安全问题。

总结来说,线程安全和可重入性是多线程编程中需要关注的重要概念。确保线程安全和函数的可重入性有助于编写出稳定、可靠的多线程程序。

常见锁的概念

在Linux中,多线程编程时经常需要使用锁来同步对共享资源的访问,以防止数据竞争和不一致。以下是一些常见的锁及其概念,并附上示范代码:

1. 互斥锁(Mutex)

概念: 互斥锁是最简单的同步原语,用于保护共享资源免受并发访问的影响。当一个线程持有互斥锁时,其他线程必须等待直到锁被释放。

示范代码

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mutex;
int shared_data = 0;

void *increment(void *arg) {
    for (int i = 0; i < 100000; ++i) {
        pthread_mutex_lock(&mutex);
        shared_data++;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL);

    if (pthread_create(&thread1, NULL, increment, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&thread2, NULL, increment, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Final shared_data value: %d\n", shared_data);
    pthread_mutex_destroy(&mutex);

    return 0;
}

2. 读写锁(Read-Write Lock)

概念: 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入。这适用于读操作远多于写操作的场景,可以提高并发性能。

示范代码

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

pthread_rwlock_t rwlock;
int shared_data = 0;

void *reader(void *arg) {
    for (int i = 0; i < 10000; ++i) {
        pthread_rwlock_rdlock(&rwlock);
        // Read shared_data
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

void *writer(void *arg) {
    for (int i = 0; i < 1000; ++i) {
        pthread_rwlock_wrlock(&rwlock);
        shared_data++;
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

int main() {
    pthread_t reader_thread, writer_thread;
    pthread_rwlock_init(&rwlock, NULL);

    if (pthread_create(&reader_thread, NULL, reader, NULL) != 0) {
        perror("Reader thread creation failed");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&writer_thread, NULL, writer, NULL) != 0) {
        perror("Writer thread creation failed");
        exit(EXIT_FAILURE);
    }

    pthread_join(reader_thread, NULL);
    pthread_join(writer_thread, NULL);

    printf("Final shared_data value: %d\n", shared_data);
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

3. 自旋锁(Spinlock)

概念: 自旋锁是一种特殊的互斥锁,当线程尝试获取锁失败时,它会忙等待(自旋)直到锁被释放。适用于锁持有时间非常短的场景,可以减少线程上下文切换的开销。

示范代码(注意:自旋锁通常通过底层API实现,如__sync_lock_test_and_set):

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

typedef volatile int spinlock_t;
spinlock_t spinlock = 0;
int shared_data = 0;

void *increment(void *arg) {
    for (int i = 0; i < 100000; ++i) {
        while (__sync_lock_test_and_set(&spinlock, 1)) {
            // 忙等待,直到锁被释放
            while (spinlock) {
                // 自旋
            }
        }
        shared_data++;
        __sync_lock_release(&spinlock);
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    if (pthread_create(&thread1, NULL, increment, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&thread2, NULL, increment, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Final shared_data value: %d\n", shared_data);

    return 0;
}

4. 条件变量(Condition Variable)

概念: 条件变量通常与互斥锁一起使用,允许线程等待某个条件成立(由其他线程通知)。它常用于实现线程间的同步。

示范代码

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

pthread_mutex_t mutex;
pthread_cond_t cond;
int ready = 0;

void *producer(void *arg) {
    pthread_mutex_lock(&mutex);
    printf("Producer: Producing item\n");
    ready = 1;
    pthread_cond_signal(&cond); // 通知消费者
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void *consumer(void *arg) {
    pthread_mutex_lock(&mutex);
    while (!ready) { // 等待生产者准备完毕
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Consumer: Consuming item\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    if (pthread_create(&producer_thread, NULL, producer, NULL) != 0) {
        perror("Producer thread creation failed");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&consumer_thread, NULL, consumer, NULL) != 0) {
        perror("Consumer thread creation failed");
        exit(EXIT_FAILURE);
    }

    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

死锁

死锁的四个必要条件

在Linux系统中,死锁通常发生在多线程或多进程环境中,当它们试图同时访问共享资源时,可能由于资源的竞争和不当的同步机制而陷入死锁状态。死锁的发生需要满足以下四个必要条件:

  1. 互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只有一个进程可以使用。如果其他进程请求该资源,则请求者只能等待,直到该资源被当前占有者释放。

  2. 持有并等待条件(Hold and Wait):一个进程至少已经持有一个资源,并正在等待获取一个当前被其他进程持有的资源。

  3. 非抢占条件(No Preemption):资源只能被占有它的进程显式地释放,资源不能被抢占。

  4. 循环等待条件(Circular Wait):存在一个进程-资源的循环等待链,其中每个进程都在等待下一个进程所持有的资源。

如何避免死锁

为了避免死锁,可以采取以下策略:

  1. 预防死锁:通过破坏死锁的四个必要条件中的一个或多个来预防死锁。

  2. 避免死锁:通过银行家算法等策略,在运行时动态地判断是否分配资源,从而避免系统进入不安全状态。

  3. 检测和解决死锁:通过定期检测死锁的发生,一旦检测到死锁,则通过抢占资源或终止进程等方法来解决死锁。

避免死锁的算法

银行家算法是一种经典的避免死锁的算法,它模拟银行家借贷的场景,在分配资源之前,判断分配后系统是否处于安全状态。如果是,则分配资源;否则,不分配资源,从而避免系统进入不安全状态。

避免死锁的示例代码

在实际编程中,避免死锁通常依赖于良好的编程习惯和同步机制的选择。以下是一个简单的示例,展示如何在多线程环境中避免死锁:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1); // 线程1先获取mutex1
    printf("Thread 1 acquired mutex 1\n");
    sleep(1); // 模拟一些工作
    pthread_mutex_lock(&mutex2); // 然后尝试获取mutex2
    printf("Thread 1 acquired mutex 2\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void *thread2(void *arg) {
    pthread_mutex_lock(&mutex2); // 线程2先获取mutex2
    printf("Thread 2 acquired mutex 2\n");
    sleep(1); // 模拟一些工作
    pthread_mutex_lock(&mutex1); // 然后尝试获取mutex1
    printf("Thread 2 acquired mutex 1\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    
    // 创建线程
    if (pthread_create(&t1, NULL, thread1, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    if (pthread_create(&t2, NULL, thread2, NULL) != 0) {
        perror("Thread creation failed");
        exit(EXIT_FAILURE);
    }
    
    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    return 0;
}

在这个例子中,两个线程分别尝试按照不同的顺序获取两个互斥锁。由于每个线程都先获取一个锁,然后尝试获取另一个锁,因此不会发生死锁。这是因为每个线程都在等待一个已经被另一个线程持有的锁,但没有形成循环等待链。

当然,在更复杂的场景中,避免死锁可能需要更复杂的策略,包括设计良好的同步协议、使用条件变量来避免忙等待、确保锁的粒度合适,以及适当地使用超时和重试机制等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1596541.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot项目整合ACTable实现实体类快速生产数据库表

1.安装 ACTable依赖 <dependency> <groupId>com.gitee.sunchenbin.mybatis.actable</groupId> <artifactId>mybatis-enhance-actable</artifactId> <version>1.5.0.RELEASE</version> </dependency> 使用mybatis…

如何做一个springboot的starter类型的SDK

关键的东西 首先我们是一个starter类型的SDK&#xff0c;为了给调用者使用&#xff0c;其中有一些Bean我们会放到SDK中&#xff0c;并且这些Bean能够注入到调用者的Spring容器中。 最关键的spring.factories文件 这个文件所在位置如下图所示&#xff0c;该文件通过写入一下代…

自定义vue-cli 实现预设模板项目

模板结构 主要包括四个部分&#xff1a; preset.jsonprompts.jsgenerator/index.jstemplate/ 项目最终结构 preset.json preset.json 中是一个包含创建新项目所需预定义选项和插件的 JSON 对象&#xff0c;让用户无需在命令提示中选择它们&#xff0c;简称预设&#xff1b;…

文献速递:深度学习肝脏肿瘤诊断---基于深度学习的肝细胞结节性病变在整片组织病理图像上的分类

Title 题目 Deep Learning-Based Classification of Hepatocellular Nodular Lesions on Whole-Slide Histopathologic Images 基于深度学习的肝细胞结节性病变在整片组织病理图像上的分类 Background 背景 Hepatocellular nodular lesions (HNLs) constitute a heterogen…

Offer必备算法26_BFS解决最短路_四道力扣题(由易到难)

目录 ①力扣1926. 迷宫中离入口最近的出口 解析代码 ②力扣433. 最小基因变化 解析代码 ③力扣127. 单词接龙 解析代码 ④力扣675. 为高尔夫比赛砍树 解析代码 本篇完。 ①力扣1926. 迷宫中离入口最近的出口 1926. 迷宫中离入口最近的出口 难度 中等 给你一个 m x …

SpringBoot学习(二)WEB开发

文章目录 WEB开发WebMvcAutoConfiguration原理生效条件效果WebMvcConfigurer接口静态资源源码规则EnableWebMvcConfiguration规则容器中WebMvcConfigurer配置底层行为 Web场景自动配置默认效果 静态资源默认规则静态资源映射静态资源缓存欢迎页Favion 自定义静态资源规则配置方…

PINet车道线检测+YOLOv8视频目标检测

前言&#xff1a; 本文主要目的是实现在PINet车道线检测的代码中嵌入YOLOv8的目标检测模块&#xff0c;具体效果如图所示&#xff1a; 在学习和使用YOLOv8进行目标检测时&#xff0c;感觉可以和最近研究的车道线检测项目结合起来&#xff0c;形成一套如上图所示的视频效…

2024/4/14周报

文章目录 摘要Abstract文献阅读题目创新点CROSSFORMER架构跨尺度嵌入层&#xff08;CEL&#xff09;CROSSFORMER BLOCK长短距离注意&#xff08;LSDA&#xff09;动态位置偏置&#xff08;DPB&#xff09; 实验 深度学习CrossFormer背景维度分段嵌入&#xff08;DSW&#xff09…

【图像分类】基于深度学习的轴承和齿轮识别(ResNet网络)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。(专栏订阅用户订阅专栏后免费提供数据集和源码一份,超级VIP用户不在服务范围之内,不想订阅专栏的兄弟们可以私信…

如何远程连接电脑?

远程连接电脑是一种技术&#xff0c;能够使用户在不同地点的电脑之间建立连接&#xff0c;实现互相访问和控制的功能。这项技术为我们提供了便利和效率&#xff0c;使得随时随地的协同办公、异地统一管理和远程数据采集管理成为可能。 【天联】的使用场景 远程连接电脑的应用非…

SAP 转储单库存可用性检查详解

客户需求在下转储单以及公司间STO时候检查发货方是否库存够,如果有库存则可以创建,没有则不让创建。以免在DN过账时候才提示库存不够,把检查库存是否充足前移。 我们知道销售单是有可用性检查功能的,那么采购转储单是否也有同样功能呢? 可用性检查控制可理解为检查组和检…

微信小程序认证指南及注意事项

如何认证小程序&#xff1f; 一、操作步骤 登录小程序后台&#xff1a; https://mp.weixin.qq.com/ (点击前往) 找到设置&#xff0c;基本设置&#xff1b; 在【基本信息】处有备案和认证入口&#xff1b; 点击微信认证的【去认证】; 按照步骤指引一步步填写信息&#xff…

使用阿里云试用Elasticsearch学习:Search Labs Tutorials 搭建一个flask搜索应用

文档&#xff1a;https://www.elastic.co/search-labs/tutorials/search-tutorial https://github.com/elastic/elasticsearch-labs/tree/main/example-apps/search-tutorial Full-Text Search

盲人出行安全保障:科技革新助力无障碍生活新纪元

作为一名资深记者&#xff0c;我有幸见证了一场科技如何深刻改变视障群体生活的壮丽篇章。在这场变革中&#xff0c;盲人出行安全保障成为焦点&#xff0c;一款融合先进科技与人文关怀的辅助应用正以前所未有的力量&#xff0c;帮助盲人朋友们打破传统束缚&#xff0c;实现安全…

每日OJ题_BFS解决最短路④_力扣675. 为高尔夫比赛砍树

目录 力扣675. 为高尔夫比赛砍树 解析代码 力扣675. 为高尔夫比赛砍树 675. 为高尔夫比赛砍树 难度 困难 你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 m x n 的矩阵表示&#xff0c; 在这个矩阵中&#xff1a; 0 表示障碍&#xff0c;无法触碰1 表示地面&…

【大语言模型】基础:余弦相似度(Cosine similarity)

余弦相似度是一种用来确定两个向量之间相似性的度量。它在数据科学、信息检索和自然语言处理&#xff08;NLP&#xff09;等多个领域被广泛使用&#xff0c;用于度量在多维空间中两个向量之间角度的余弦。这个指标捕捉的是方向上的相似性而非大小&#xff0c;使其非常适合比较长…

专业SEO优化指南:设置网站关键词的详细步骤

在网站SEO优化的过程中&#xff0c;关键词的设置是提升网站排名的关键步骤之一。那么&#xff0c;作为一名专业的SEO人员&#xff0c;如何有效地进行关键词设置呢&#xff1f;以下是一些详细的步骤&#xff1a; 1. 确定网站的核心关键词。 这需要深入理解网站的主题或产品。通…

结合创新!ResNet+Transformer,高性能低参数,准确率达99.12%

今天给各位介绍一个发表高质量论文的好方向&#xff1a;ResNet结合Transformer。 ResNet因其深层结构和残差连接&#xff0c;能够有效地从图像中提取出丰富的局部特征。同时&#xff0c;Transformer的自注意力机制能够捕捉图像中的长距离依赖关系&#xff0c;为模型提供全局上…

GPT人工智能在线网页版大全

平民不参与内测&#xff0c;还能使用 ChatGPT 吗&#xff1f; 自去年 ChatGPT 爆红以来&#xff0c;关于它的消息铺天盖地。如果你真的想使用它&#xff0c;途径有很多。除了官方网站外国内还有许多 ChatGPT 的镜像网站&#xff0c;其中不乏免费的 3.5 版本。虽然有些网站需要…

byobu

byobu 终端多路复用器 一、byobu 安装二、byobu 使用三、其他终端多路复用器四、ssh byobu 远程协作 系统环境: linux(ubuntu,debian,kali) 一、byobu 安装 byobu 是包装过的tmux #sudo apt install tmux sudo apt install byobubyobu二、byobu 使用 创建窗口: Ctrl a c…