Linux系统编程之线程

什么是线程:

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程(Process)中,是进程中的实际执行单元。一个进程可以有多个线程,这些线程共享进程的资源,包括内存空间、文件描述符等。 与进程相比,线程具有以下特点:

  1. 轻量性:相对于进程来说,线程的创建、销毁和切换开销较小,因为它们共享相同的地址空间和其他资源。
  2. 并发性:多个线程可以同时执行,从而提高系统的并发处理能力。
  3. 共享资源:线程共享所属进程的资源,包括内存、文件、设备等。这使得线程之间可以方便地进行数据共享和通信。
  4. 互斥性:由于线程共享资源,因此需要采取适当的同步机制,以避免多个线程同时访问和修改共享资源导致的数据不一致性和竞态条件。 线程可以归类为用户线程和内核线程。用户线程由用户程序自行管理和调度,而内核线程由操作系统内核负责管理和调度。内核线程更具有通用性和可靠性,但创建和切换的开销相对较大。 线程常常用于需要同时进行多个任务的应用程序,例如并发服务器、多线程编程模型、图形界面应用程序等。通过合理地使用线程,可以提高程序的性能和响应速度,充分利用多核处理器的计算能力。 需要注意的是,线程之间的并发访问共享资源可能会导致数据竞争和死锁等问题,因此在多线程编程中,需要仔细设计和使用同步机制,如互斥锁、条件变量、原子操作等,以保证线程之间的正确协作和数据一致性。

总结:

LWP:轻量级的进程,本质仍然是进程(Linux环境下)。

线程:有独立的PCB,但木有独立的地址空间(共享)。

进程:独立的地址空间,拥有PCB。

区别:在于是否共享地址空间, 独居(进程) 合租(线程)

Linux下:

线程:最小的执行单位。

进程:最小分配资源单位,可看成只有一个线程的进程。

Linux内核线程实现原理

类似unix系统中,早期是木有“线程”概念的,80年引入,借助进程机制实现出线程的概念。因此在这类系统中,进程和线程关系密切。

1.轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone。

什么是clone?

在Linux中,clone是一个系统调用,用于创建一个新的进程或线程。clone系统调用与fork系统调用类似,但它提供了更多的灵活性和控制选项。 clone系统调用的原型如下:

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...);

参数解释如下:

  • fn:指向新进程或线程要执行的函数的指针。该函数在新进程或线程创建后会被调用,可以在其中执行特定的任务。
  • child_stack:指向新进程或线程的堆栈的指针。在创建新进程或线程时,需要为其分配一块独立的堆栈空间。
  • flags:用于指定创建新进程或线程的选项和行为。可以使用不同的标志来控制新进程或线程的特性,比如共享地址空间、共享文件描述符等。
  • arg:传递给新进程或线程函数的参数,可以是任意类型的指针。 clone系统调用的返回值为新创建进程或线程的进程ID(PID),或者在出现错误时返回-1。 通过clone系统调用,可以实现以下功能:
  • 创建一个新的进程或线程,与调用进程共享一部分资源。
  • 控制新进程或线程的特性,如共享地址空间、共享文件描述符等。
  • 在新进程或线程中执行指定的函数,完成特定的任务。 需要注意的是,clone系统调用是一个底层调用,通常需要结合其他系统调用和库函数来实现更高级的功能,如进程间通信、线程同步等。在实际使用中,应根据具体需求选择适当的调用方式和选项,以达到期望的效果。

2.从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的

内存资源的三级页表是什么?

在操作系统中,PCB(Process Control Block)是描述进程的数据结构,用于存储和管理进程的信息。而三级页表是一种用于虚拟内存管理的数据结构。 虚拟内存是一种操作系统提供的抽象概念,它允许进程访问比物理内存更大的地址空间。为了实现虚拟内存的功能,操作系统使用页表来映射进程的虚拟地址空间到物理内存中的实际物理地址。 在一些系统中,为了有效地管理大型的虚拟地址空间,页表采用了多级结构,其中三级页表就是其中一种常见的组织方式。三级页表将整个虚拟地址空间划分为多个不同级别的页表,并通过指针来链接这些页表。 具体来说,三级页表由三个级别的页表组成,包括一级页表、二级页表和三级页表。每个级别的页表包含多个表项,每个表项描述了虚拟地址和物理地址的映射关系。 当进程访问虚拟地址时,操作系统需要通过三级页表来确定该虚拟地址对应的物理地址。首先,操作系统利用一级页表找到二级页表的物理地址,再利用二级页表找到三级页表的物理地址,最后通过三级页表找到对应的物理地址。 使用三级页表的好处是可以更加灵活地管理大型的虚拟地址空间,减少内存的占用和访问的时间复杂度。同时,三级页表也可以提高地址映射的精度和效率,更好地满足进程的内存管理需求。 需要注意的是,不同的操作系统可能采用不同的页表结构和级数,三级页表只是其中一种常见的实现方式。在实际的操作系统中,页表的结构和级数可能会根据系统的需求和设计进行调整。

及三级映射:进程PCB→页目录(可以看成数组,首地址位于PCB中)→页表→物理页面→内存单元。

3.进程可以蜕变成线程。

在操作系统中,进程和线程都是执行程序的基本单位。进程是资源分配的基本单位,而线程是CPU调度的基本单位。在某些情况下,一个进程可以蜕变(也称为转换)成一个线程。 进程蜕变成线程通常发生在多线程编程或并发编程中,具体情况如下:

  1. 多线程编程:当一个进程创建了多个线程时,这些线程会共享进程的资源,如内存空间、文件描述符等。在这种情况下,可以将一个进程视为主线程,其他线程可以看作是子线程。因此,主线程与子线程之间存在一种关系,主线程可以看作是一个具有特殊功能的线程,但它仍然是一个线程,只是在进程中的地位比较特殊。
  2. 转换过程:在多线程编程中,有时会将某个线程从一个进程中分离出来,然后将其附加到另一个进程中。这个过程称为线程的转换或蜕变。通过这种方式,一个线程可以从一个进程的上下文切换到另一个进程的上下文,继续执行其他任务。 需要注意的是,进程和线程之间存在一些差异。进程是一个独立的执行环境,具有自己的地址空间和资源,而线程是在进程内部创建的,共享进程的资源。蜕变过程中,线程的上下文会被切换到新的进程上下文中,因此线程在新的进程中执行时,会继承新进程的资源和环境。
  3. 总结来说,进程可以蜕变成线程是指在多线程编程中,一个线程可以从一个进程中分离出来,然后附加到另一个进程中,继续执行其他任务。这种转换或蜕变过程可以实现不同进程之间的线程共享和协作。

4.线程可看作寄存器和栈集合。

线程可以被看作是寄存器和栈的集合,是因为线程的执行状态主要由寄存器和栈来维护。

  1. 寄存器:每个线程都有自己的寄存器集合,包括通用寄存器、指令指针寄存器、栈指针寄存器等。这些寄存器用于保存线程执行过程中的临时数据和状态信息。通过寄存器,线程可以保存和恢复其执行上下文,以便在切换到其他线程后能够继续执行。
  2. 栈:每个线程都有自己的栈空间,用于存储局部变量、函数调用信息和临时数据等。栈是一种后进先出(LIFO)的数据结构,用于管理函数调用、函数返回和局部变量的生命周期。线程的栈主要用于保存函数调用的返回地址、函数参数和局部变量等信息。 当线程被创建时,操作系统为其分配一块独立的栈空间,并为线程分配一组寄存器。在线程执行期间,寄存器和栈被用于保存线程的执行上下文和临时数据。当线程被调度执行时,其上下文信息(包括寄存器的值和栈的状态)被加载到处理器中,线程开始执行指令。当线程被切换出去时,其上下文信息被保存,以便下次恢复执行。
  3. 总结来说,线程可以被看作是寄存器和栈的集合,因为寄存器用于保存线程的执行上下文和临时数据,而栈用于管理函数调用和临时数据的生命周期。寄存器和栈是线程执行过程中维护和管理状态的重要组成部分。

5.在Linux下,线程是最小的执行单元,进程是最小的分配资源单位。

如何查看LWP号:ps -Lf pid 查看指定线程的lwp。

对于线程来说,相同的地址在不同的进程中,反复使用而不冲突,原因是他们虽然虚地址一样,但是三级页表各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但是,线程不一样!俩个线程具有各自独立的PCB,但是共分享同一个页目录,也就是共享同一个页表和物理页面,所以俩个PCB共享同一个地址空间。

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现但是调用同一个内核函数clone。

如果复制对方的地址空间,那么就产生出一个“进程”;如果共享对方的地址空间,那么产生一个“线程“。

因此:Linux内核是不区分线程和线程的,只在用户层上就行区分。所以,线程所有的操作函数pthread*是库函数,而非系统调用。

线程共享资源

1.文件描述表

什么是文件描述表?

文件描述符表(File Descriptor Table)是操作系统内核用于管理打开文件的一种数据结构。在Unix-like系统中,每个进程都有一个文件描述符表,用于跟踪它所打开的文件和其他I/O资源。 文件描述符是一个非负整数,它是对打开文件或其他I/O资源的引用。当进程打开一个文件时,操作系统会分配一个文件描述符,并将其添加到进程的文件描述符表中。文件描述符是进程与打开的文件之间的接口,通过它进程可以对文件进行读、写、关闭等操作。 在传统的Unix系统中,文件描述符的前三个值是预留的:

  • 0:标准输入(stdin)
  • 1:标准输出(stdout)
  • 2:标准错误(stderr) 其他文件描述符从3开始递增。当进程打开文件时,操作系统会找到文件描述符表中的一个未被使用的位置,将其分配给新打开的文件。文件描述符的具体实现可以是数组、链表或其他数据结构,不同的操作系统可能有不同的实现方式。 文件描述符表的作用包括:
  1. 标识打开的文件和其他I/O资源: 每个文件描述符对应着一个打开的文件、管道、套接字等。通过文件描述符,进程可以唯一标识和操作这些I/O资源。
  2. 实现文件共享: 多个进程可以共享相同的文件描述符,从而实现文件共享。例如,一个进程打开一个文件,另一个进程通过继承相同的文件描述符也可以对同一个文件进行操作。
  3. 进行I/O操作: 进程可以通过文件描述符进行读取、写入、定位等文件操作。文件描述符提供了一种抽象,使得进程无需关心底层的文件实现细节。
  4. 实现进程间通信: 在某些情况下,文件描述符也被用于实现进程间通信,例如通过管道或套接字进行数据传输。 总的来说,文件描述符表是操作系统用于管理进程打开文件和其他I/O资源的关键数据结构,提供了一种标识和操作文件的抽象接口。

2.每种信号的处理方式

解释如下:

线程共享资源时,信号的处理方式可以通过信号处理器(Signal Handler)来定义。在多线程环境中,不同线程可以有不同的信号处理方式。以下是一些常见的线程共享资源时信号的处理方式:

  1. 默认处理方式(Default Action): 默认情况下,线程对信号的处理方式通常是继承自父线程或整个进程。例如,对于 SIGINT 信号(中断信号),默认处理方式是终止进程。
  2. 忽略信号(Ignore): 线程可以选择忽略某些信号。通过设置信号处理器为 SIG_IGN,线程将忽略接收到的特定信号。这样可以防止某些信号对线程的影响。
  3. 捕获和处理信号(Custom Handler): 线程可以注册自定义的信号处理器来捕获和处理特定信号。通过设置信号处理器为一个自定义函数,线程可以在接收到信号时执行特定的操作,例如释放资源、记录日志等。
  4. 线程取消(Thread Cancellation): 在多线程环境中,线程可能会被取消。取消信号(SIGCANCEL)用于通知线程它将被取消。线程可以选择在接收到取消信号时执行清理操作,或者忽略取消请求。 需要注意的是,线程共享资源时的信号处理方式可能会受到线程创建时的属性设置的影响。例如,线程的属性中可能包含了信号屏蔽集合,影响了线程在某些情况下是否会响应特定的信号。

3.当前工作目录

4.用户id和组id

5.内存地址空间

线程非共享资源

1.线程id

2.理器现场指和栈指针(内核栈)

3.独立的栈空间(用户空间)

4.errno变量

5.信号屏蔽字

6.调度优先级

线程优缺点

优点:

1.提高程序的并发性

2.开销小

3.数据通信,共享数据方便

缺点:

1.库函数不稳定

2.调试,编写困难,gdb不支持

3.对信号不好

线程控制原语

pthread_self函数

pthread_self函数是一个POSIX线程库中的函数,用于获取当前线程的线程ID(Thread ID)。它的函数原型如下:

pthread_t pthread_self(void);

该函数没有参数,返回值是pthread_t类型的线程ID。pthread_t实际上是一个线程ID的数据类型,具体实现可能是一个整数或者一个结构体。 pthread_self函数的作用是获取当前线程的线程ID,即调用该函数的线程的唯一标识。通过线程ID,可以在程序中区分和操作不同的线程。 需要注意的是,线程ID是由操作系统内核分配和管理的,它是唯一且不可变的。在一个进程中,每个线程都有自己独立的线程ID。 使用pthread_self函数的示例代码如下:

#include <pthread.h>
#include <stdio.h>
void* thread_function(void* arg) {
    pthread_t thread_id = pthread_self();
    printf("Thread ID: %lu\\\\n", thread_id);
    // 线程的其他操作
    return NULL;
}
int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    pthread_join(thread, NULL);
    return 0;
}

在上述示例中,pthread_self函数在thread_function中被调用,用于获取当前线程的线程ID,并打印出来。通过这个例子,可以看到不同线程具有不同的线程ID。 总结起来,pthread_self函数是用于获取当前线程的线程ID的函数。它对于识别和操作线程非常有用,可以在多线程程序中进行线程管理和调试。

pthread_self函数的返回值是当前线程的线程ID(Thread ID)。线程ID是一个唯一标识符,用于在程序中区分和操作不同的线程。 具体来说,pthread_self函数返回一个pthread_t类型的值,表示当前线程的线程ID。pthread_t实际上是一个线程ID的数据类型,具体实现可能是一个整数或者一个结构体。 线程ID是由操作系统内核分配和管理的,它是唯一且不可变的。在一个进程中,每个线程都有自己独立的线程ID。 使用pthread_self函数的示例代码如下:

#include <pthread.h>
#include <stdio.h>
void* thread_function(void* arg) {
    pthread_t thread_id = pthread_self();
    printf("Thread ID: %lu\\\\n", thread_id);
    // 线程的其他操作
    return NULL;
}
int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    pthread_join(thread, NULL);
    return 0;
}

在上述示例中,pthread_self函数在thread_function中被调用,用于获取当前线程的线程ID,并打印出来。通过这个例子,可以看到不同线程具有不同的线程ID。 所以,pthread_self函数的返回值表示当前线程的线程ID,可以用于在程序中区分和操作不同的线程。

pthread_create函数

pthread_create函数是POSIX线程库中的函数,用于创建一个新的线程。它的函数原型如下:

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

该函数的参数说明如下:

  • thread:一个指向pthread_t类型的指针,用于存储新线程的线程ID。在函数成功返回时,通过该指针可以获取新线程的线程ID。
  • attr:一个指向pthread_attr_t类型的指针,用于指定新线程的属性。可以为NULL,表示使用默认的线程属性。
  • start_routine:一个指向函数的指针,该函数将作为新线程的起始函数。该函数的返回类型必须是void*,参数为void*类型。新线程将从该函数开始执行。
  • arg:传递给start_routine函数的参数。可以是任意类型的指针,通过该参数可以向新线程传递数据。 pthread_create函数的返回值为整型,成功创建新线程时返回0,失败时返回一个非零的错误码。 使用pthread_create函数创建新线程的示例代码如下:
#include <pthread.h>
#include <stdio.h>
void* thread_function(void* arg) {
    int thread_id = *(int*)arg;
    printf("Hello from thread %d\\\\n", thread_id);
    // 线程的其他操作
    return NULL;
}
int main() {
    pthread_t thread;
    int thread_id = 1;
    pthread_create(&thread, NULL, thread_function, &thread_id);
    pthread_join(thread, NULL);
    return 0;
}

在上述示例中,pthread_create函数用于创建一个新线程,并通过thread_function作为新线程的起始函数。同时,通过arg参数将线程ID传递给新线程进行使用。 总结起来,pthread_create函数用于创建新线程,通过指定线程的属性、起始函数以及传递参数来实现。它是实现多线程编程的重要函数之一,可以在程序中创建并发执行的线程。

在使用pthread_create函数创建新线程时,它的返回值代表着函数的执行结果。 具体来说,pthread_create函数的返回值为一个整数类型(int),它表示了函数执行的成功与否。以下是pthread_create函数返回值的含义:

  • 成功创建新线程:如果pthread_create函数成功创建了新线程,它将返回0作为函数的返回值。
  • 创建新线程失败:如果pthread_create函数在创建新线程时发生错误,它将返回一个非零的错误码,用于表示具体的错误类型。这些错误码可以通过查阅相关的文档或使用strerror函数来进行解释和处理。 在实际编程中,可以根据pthread_create函数的返回值来判断线程是否成功创建,并根据不同的返回值进行相应的错误处理或调试。 以下是一个使用pthread_create函数并处理返回值的示例代码:
#include <pthread.h>
#include <stdio.h>
#include <string.h>
void* thread_function(void* arg) {
    // 线程的操作
    return NULL;
}
int main() {
    pthread_t thread;
    int result = pthread_create(&thread, NULL, thread_function, NULL);
    if (result != 0) {
        printf("Error creating thread: %s\\\\n", strerror(result));
        return 1;
    }

    // 主线程的操作

    pthread_join(thread, NULL);
    return 0;
}

在上述示例中,通过检查pthread_create函数的返回值result,如果不等于0,表示创建新线程失败。在这种情况下,可以使用strerror函数将错误码转换为可读的错误信息,并进行相应的错误处理。 因此,pthread_create函数的返回值可以帮助我们判断线程的创建是否成功,并根据返回值进行相应的处理。

pthread_exit函数:

pthread_exit函数用于终止当前线程的执行,并返回一个指定的值。下面是对pthread_exit函数的详细解释和参数说明: 函数解释: pthread_exit函数允许线程提前退出并返回一个指定的值。调用pthread_exit函数后,线程的执行将立即终止,不会执行后续的代码。 参数说明:

  • value_ptr:一个指向void类型的指针,用于传递线程的返回值。线程的返回值可以是任意类型的指针或整数。如果不需要返回值,可以传递NULL。 返回值说明: pthread_exit函数本身没有返回值。它会将value_ptr指向的值作为线程的返回值。如果其他线程通过pthread_join函数等方式等待并成功获取了返回值,那么该返回值将被传递给等待线程。 需要注意的是,调用pthread_exit函数后,线程的资源(如栈、寄存器等)会被释放,但是线程的状态信息(如线程ID、终止状态等)会继续保留,以供其他线程查询和处理。 另外,当主线程调用pthread_exit函数时,它将会终止整个进程,而不仅仅是当前线程。 总结:pthread_exit函数用于终止当前线程的执行,并返回一个指定的值。通过value_ptr参数可以指定线程的返回值。调用pthread_exit后,线程的执行将立即终止,不会执行后续的代码。其他线程可以通过pthread_join函数等方式等待并获取该线程的返回值。

举例说明:

假设我们有一个多线程程序,其中包含两个线程:主线程和子线程。主线程负责创建子线程,并等待子线程执行完毕后获取其返回值。 下面是一个示例代码,展示了pthread_exit函数的使用:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg) {
    int *value = (int *)arg;
    printf("子线程开始执行\\\\n");

    // 子线程执行一些操作...
    *value = 100; // 设置返回值为100

    printf("子线程执行完毕\\\\n");
    pthread_exit((void *)value); // 终止子线程,并返回value作为返回值
}
int main() {
    pthread_t thread;
    int ret_value;

    printf("主线程开始执行\\\\n");

    // 创建子线程
    pthread_create(&thread, NULL, thread_function, (void *)&ret_value);

    // 等待子线程执行完毕并获取其返回值
    pthread_join(thread, (void **)&ret_value);
    printf("子线程返回值: %d\\\\n", ret_value);

    printf("主线程执行完毕\\\\n");
    return 0;
}

在上述示例中,主线程通过pthread_create函数创建了一个子线程,并传递了一个指向ret_value的指针作为参数。子线程在执行过程中将ret_value设置为100,并通过pthread_exit函数终止自身的执行。 主线程使用pthread_join函数等待子线程执行完毕,并通过指针ret_value获取子线程的返回值。 输出结果可能为:

主线程开始执行
子线程开始执行
子线程执行完毕
子线程返回值: 100
主线程执行完毕

可以看到,子线程通过pthread_exit函数返回了一个值,并由主线程获取并打印出来。 这个例子展示了pthread_exit函数的使用方式,通过传递参数并在子线程中设置返回值,实现了线程之间的数据传递。

在上述示例代码中,我们使用了一个参数arg来传递给子线程的函数thread_function。这个参数的具体含义是根据实际需求而定,可以用来传递任意类型的数据。 在示例中,我们将ret_value作为参数传递给子线程函数。具体解释如下:

  • arg参数的类型是void *,这是一个通用类型的指针,可以指向任意类型的数据。
  • pthread_create函数调用时,我们通过(void *)&ret_valueret_value的地址强制转换为void *类型的指针,然后传递给子线程函数。
  • 在子线程函数中,我们将arg参数转换为int *类型,并将其赋值给value变量,即int *value = (int *)arg;。这样子线程就可以使用value指向的内存来操作传递进来的值。
  • 在子线程函数中,我们将value设置为100,即value = 100;。这样就修改了ret_value所指向的内存中的值。
  • 最后,在调用pthread_exit函数时,我们将value指针强制转换为void *类型,并作为返回值传递给主线程,即pthread_exit((void *)value);。 通过这种方式,我们可以在子线程中修改ret_value的值,并通过pthread_exit函数将修改后的值传递给主线程,实现了线程之间的数据传递。 需要注意的是,对于参数传递,需要确保传递的数据类型和指针类型的一致性,以避免类型转换错误和内存访问问题。

pthread_join函数:

pthread_join函数用于等待指定的线程终止,并获取其返回值。下面是对pthread_join函数的详细解释和参数说明: 函数解释: pthread_join函数用于等待指定的线程终止,并获取其返回值。调用pthread_join函数后,当前线程将阻塞,直到指定的线程终止。 参数说明:

  • thread:一个pthread_t类型的参数,表示要等待的目标线程。该参数是被等待线程的标识符。
  • value_ptr:一个指向指针的指针(void **),用于接收被等待线程的返回值。被等待线程通过pthread_exit函数返回的值将被存储在value_ptr指向的内存中。如果不需要获取返回值,可以传递NULL。 返回值说明: pthread_join函数返回一个整数,表示函数执行的成功与否。如果成功等待线程的终止,则返回0;如果出现错误,则返回一个非零的错误码。 需要注意的是,被等待的线程必须是可连接的,即它的终止状态必须是可获取的。如果被等待的线程已经被其他线程分离(使用pthread_detach函数),那么调用pthread_join函数将会失败。 另外,如果多个线程同时等待同一个线程的终止,那么只有一个线程能够成功等待,其他线程将返回错误。 总结:pthread_join函数用于等待指定的线程终止,并获取其返回值。通过thread参数指定要等待的线程,通过value_ptr参数接收被等待线程的返回值。调用pthread_join函数后,当前线程将阻塞,直到指定的线程终止。函数返回一个整数,表示执行的成功与否。

假设我们有一个多线程程序,其中包含两个线程:主线程和子线程。主线程负责创建子线程,并等待子线程执行完毕后获取其返回值。 下面是一个示例代码,展示了pthread_join函数的使用:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg) {
    int *value = (int *)arg;
    printf("子线程开始执行\\\\n");

    // 子线程执行一些操作...
    *value = 100; // 设置返回值为100

    printf("子线程执行完毕\\\\n");
    pthread_exit((void *)value); // 终止子线程,并返回value作为返回值
}
int main() {
    pthread_t thread;
    int ret_value;

    printf("主线程开始执行\\\\n");

    // 创建子线程
    pthread_create(&thread, NULL, thread_function, (void *)&ret_value);

    // 等待子线程执行完毕并获取其返回值
    pthread_join(thread, (void **)&ret_value);
    printf("子线程返回值: %d\\\\n", ret_value);

    printf("主线程执行完毕\\\\n");
    return 0;
}

在上述示例中,主线程通过pthread_create函数创建了一个子线程,并传递了一个指向ret_value的指针作为参数。子线程在执行过程中将ret_value设置为100,并通过pthread_exit函数终止自身的执行。 主线程使用pthread_join函数等待子线程执行完毕,并通过指针ret_value获取子线程的返回值。 输出结果可能为:

主线程开始执行
子线程开始执行
子线程执行完毕
子线程返回值: 100
主线程执行完毕

可以看到,子线程通过pthread_exit函数返回了一个值,并由主线程获取并打印出来。 这个例子展示了pthread_join函数的使用方式,通过等待子线程的终止并获取其返回值,实现了线程之间的同步和数据传递。

简单来说:阻塞等待线程退出,获取线程退出状态 其作用,对应进程中 waitpid()函数。

int pthread_join(pthread_t thread,void** retval); 成功:0 失败:错误号

参数: thread:线程id;retval:存储线程结束状态

对比记忆:

进程中:main返回值 exit参数→int 等待子进程结束 wait函数参数→int*

线程中:线程主函数返回值 pthread_exit→void* 等待线程结束 pthread_join 函数参数→void**

pthread_detach函数:

pthread_detach函数用于将指定的线程设置为分离状态,从而使得线程在终止时可以自动释放资源,不需要其他线程调用pthread_join函数来等待其终止。 下面是对pthread_detach函数的详细解释和参数说明: 函数解释: pthread_detach函数将指定的线程设置为分离状态,以实现线程的自动资源释放。 参数说明:

  • thread:一个pthread_t类型的参数,表示要设置为分离状态的目标线程。该参数是被分离线程的标识符。 返回值说明: pthread_detach函数返回一个整数,表示函数执行的成功与否。如果成功将线程设置为分离状态,则返回0;如果出现错误,则返回一个非零的错误码。 需要注意的是,被设置为分离状态的线程一旦终止,它所占用的系统资源将会自动释放,而不需要其他线程调用pthread_join函数来等待其终止。因此,被设置为分离状态的线程不再是可连接的。 另外,如果一个线程被设置为分离状态,而其他线程仍然在等待该线程的终止(通过pthread_join函数等待),那么该线程的资源将不会被释放,直到所有等待它的线程都调用了pthread_join函数。 总结:pthread_detach函数用于将指定的线程设置为分离状态,使得线程在终止时可以自动释放资源。被设置为分离状态的线程不再是可连接的,不需要其他线程调用pthread_join函数来等待其终止。函数返回一个整数,表示执行的成功与否。

下面是一个示例代码,展示了pthread_detach函数的使用:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void *arg) {
    printf("子线程开始执行\\\\n");

    // 子线程执行一些操作...

    printf("子线程执行完毕\\\\n");
    pthread_detach(pthread_self()); // 设置当前线程为分离状态

    pthread_exit(NULL); // 终止子线程
}
int main() {
    pthread_t thread;

    printf("主线程开始执行\\\\n");

    // 创建子线程
    pthread_create(&thread, NULL, thread_function, NULL);

    // 继续主线程执行其他操作...

    printf("主线程执行完毕\\\\n");
    return 0;
}

在上述示例中,主线程通过pthread_create函数创建了一个子线程,并传递了一个空指针作为参数。子线程在执行过程中执行一些操作,然后通过pthread_detach函数将自身设置为分离状态。 主线程继续执行其他操作,并最终结束。 在这个例子中,子线程被设置为分离状态后,当子线程终止时,系统会自动释放其占用的资源,不需要主线程调用pthread_join函数来等待子线程的终止。 需要注意的是,被设置为分离状态的线程一旦终止,它所占用的系统资源将会自动释放,因此在主线程中创建的子线程可以在不影响主线程运行的情况下独立执行。 总结:通过调用pthread_detach函数,可以将指定的线程设置为分离状态,使得线程在终止时可以自动释放资源。这种方式适用于主线程不需要等待子线程终止的情况,可以提高程序的灵活性和效率。

pthread_cancel函数:

pthread_cancel 函数是 POSIX 线程库中提供的一个函数,用于请求取消指定的线程。该函数的原型如下:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

pthread_cancel 函数的作用是向指定的线程发送取消请求。当线程收到取消请求时,它可以选择在适当的时候终止自己的执行。需要注意的是,线程只有在执行到取消点(cancellation point)时才能响应取消请求,否则取消请求将在下一个取消点等待。 取消点是程序中可以安全取消线程的点。常见的取消点包括线程阻塞在某些系统调用、等待条件变量、等待互斥锁等地方。在这些点上,线程可以被取消而不会导致数据不一致或资源泄漏。 使用 pthread_cancel 函数的基本流程如下:

  1. 调用 pthread_cancel 函数,向目标线程发送取消请求。
pthread_cancel(thread_id);

  1. 目标线程在适当的取消点响应取消请求,并执行取消清理工作。
void cleanup_handler(void *arg) {
    // 清理工作
}
void* thread_function(void* arg) {
    // 设置取消点处的清理函数
    pthread_cleanup_push(cleanup_handler, NULL);
    // 线程执行的代码
    // 弹出清理函数
    pthread_cleanup_pop(1); // 1 表示执行清理函数
    pthread_exit(NULL);
}

需要注意的是,取消点的设置和取消清理函数的使用可以提高在取消线程时的安全性。此外,一般不建议突然地取消线程,而是在线程执行的逻辑中适当地设置取消点,使线程在可以安全取消的地方响应取消请求。 总结:pthread_cancel 函数用于向指定线程发送取消请求,但线程需要在适当的取消点响应取消请求并进行清理工作。

控制原语对比:

在并发编程中,控制原语用于协调多个线程之间的执行顺序和访问共享资源的方式。

  1. 互斥锁(Mutex): 互斥锁是一种最常见的控制原语,用于保护临界区(一段代码或共享资源)的互斥访问。互斥锁提供了两个基本操作:加锁和解锁。只有获得锁的线程可以进入临界区,其他线程需要等待。互斥锁保证了同一时间只有一个线程可以访问临界区,从而避免了数据竞争和不一致性。
  2. 条件变量(Condition Variable): 条件变量用于线程间的等待和通知机制,常与互斥锁一起使用。一个线程可以等待条件变量的某个条件为真,而另一个线程可以通过发送信号或广播通知等方式,使等待的线程重新竞争互斥锁。条件变量是用于线程间协调和同步的重要机制。
  3. 信号量(Semaphore): 信号量是一种计数器,用于控制同时访问共享资源的线程数量。信号量可以初始化为一个正整数,每次线程进入临界区时,信号量减一;当线程离开临界区时,信号量加一。当信号量为零时,其他线程需要等待。信号量可以用于解决生产者-消费者问题、限制资源的访问数量等场景。
  4. 屏障(Barrier): 屏障用于将多个线程分成多个阶段并进行同步。在屏障处,所有线程需要等待,直到所有线程都到达屏障点,然后才能继续执行。屏障可以用于同步多个线程的执行步骤,确保某些操作在所有线程完成之前不会执行。
  5. 读写锁(Read-Write Lock): 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁提供了两种模式的锁定:读模式和写模式。当多个线程以读模式访问共享资源时,不会互斥;但当线程以写模式访问共享资源时,其他线程无法同时以读或写模式访问。

线程属性

1.线程属性的初始化

在使用线程时,可以通过线程属性(Thread Attribute)来对线程进行一些配置和初始化。线程属性是一个结构体,用于设置线程的各种属性,例如栈大小、分离状态、调度策略等。下面是一种常见的线程属性初始化流程:

  1. 定义并初始化线程属性对象: 首先,需要定义一个 pthread_attr_t 类型的变量,并使用 pthread_attr_init 函数对其进行初始化。
pthread_attr_t attr;
pthread_attr_init(&attr);

  1. 设置线程属性: 接下来,可以使用一系列的线程属性设置函数来修改线程属性对象的属性。例如,可以使用 pthread_attr_setstacksize 设置线程栈的大小,使用 pthread_attr_setdetachstate 设置线程的分离状态等。
pthread_attr_setstacksize(&attr, stack_size);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
// 其他属性设置

  1. 创建线程并使用线程属性: 最后,使用 pthread_create 函数创建线程时,将线程属性对象作为参数传递给该函数。
pthread_t thread;
pthread_create(&thread, &attr, start_routine, arg);

  1. 销毁线程属性对象(可选): 在不再使用线程属性对象时,可以使用 pthread_attr_destroy 函数进行销毁,以释放相关资源。
pthread_attr_destroy(&attr);

需要注意的是,线程属性的初始化和设置应在创建线程之前完成。线程属性的设置是可选的,系统会提供默认的属性值。如果不需要对线程进行特殊配置,可以直接使用默认的属性。 以上是一种常见的线程属性初始化流程,具体的属性设置和使用方式可以根据实际需求进行调整。在使用线程属性时,应该了解各个属性的含义和影响,并根据具体场景进行合理的设置。

2.线程的分离状态

线程的分离状态是指线程在结束时是否会保留其终止状态信息供其他线程收集。一个线程可以被设置为分离状态,也可以是非分离状态。 在创建线程时,可以通过设置线程属性的方式来指定线程的分离状态。具体来说,可以使用 pthread_attr_setdetachstate 函数将线程属性中的分离状态设置为 PTHREAD_CREATE_DETACHEDPTHREAD_CREATE_JOINABLE

  1. 分离状态(Detached State):
    • 如果将线程设置为分离状态,意味着线程在结束时会立即释放其资源,不再保留终止状态信息。这样的线程被称为分离线程。
    • 分离线程无法被其他线程通过 pthread_join 函数来等待,也不能被取消。线程的资源会在其结束时自动释放,无需其他线程显式地调用 pthread_join
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

  1. 可连接状态(Joinable State):
    • 如果将线程设置为可连接状态,意味着线程在结束时会保留其终止状态信息,允许其他线程通过 pthread_join 函数来等待该线程的结束,并获取其终止状态。
    • 可连接线程在结束时需要被显式地由其他线程调用 pthread_join 来等待,否则线程的资源可能无法被正确释放。
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

在实际使用中,选择线程的分离状态取决于程序的需求。如果不关心线程的结束状态,且希望系统能够更自由地管理线程的资源,可以选择将线程设置为分离状态。如果需要等待线程结束并获取其终止状态,就选择可连接状态。 需要注意的是,一旦线程被创建并开始执行,就不能再改变其分离状态。因此,在创建线程之前,需要确保已经设置了期望的线程属性。

3.线程属性控制示例

下面是一个简单的示例,演示如何使用线程属性来控制线程的分离状态:

#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
    printf("Thread is running\\\\n");
    pthread_exit(NULL);
}
int main() {
    pthread_t thread;
    pthread_attr_t attr;
    // 初始化线程属性
    pthread_attr_init(&attr);
    // 设置线程属性为分离状态
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 创建线程,并传递线程属性
    pthread_create(&thread, &attr, thread_function, NULL);
    // 销毁线程属性
    pthread_attr_destroy(&attr);
    printf("Main thread is exiting\\\\n");
    pthread_exit(NULL);
}

在上述示例中:

  1. 首先,通过 pthread_attr_init 函数初始化线程属性对象 attr
  2. 然后,使用 pthread_attr_setdetachstate 函数将线程属性设置为分离状态。
  3. 接下来,使用 pthread_create 函数创建线程,并将线程属性对象作为参数传递给该函数,确保新创建的线程是分离线程。
  4. 最后,使用 pthread_attr_destroy 函数销毁线程属性对象。
  5. 主线程打印一条消息后退出,分离线程在后台运行并打印一条消息。 需要注意的是,分离线程在结束时会立即释放其资源,因此我们不需要调用 pthread_join 来等待它的结束。主线程在退出之前,会等待创建的分离线程完成其执行。

4.线程使用注意事项

在使用线程时,有一些注意事项需要特别注意,以确保线程的正确、可靠和高效运行:
1. **线程同步:** 多个线程同时访问共享资源时,需要使用适当的同步机制来确保线程安全。常见的同步机制包括互斥锁、条件变量、信号量等,可以避免数据竞争和不一致性。
2. **资源管理:** 确保在线程使用完共享资源后,及时释放资源,避免资源泄漏。例如,在使用完互斥锁后,需要及时解锁;在使用完动态分配的内存后,需要及时释放。
3. **避免死锁:** 当多个线程相互等待对方释放资源时,可能发生死锁。为了避免死锁,需要合理地设计锁的获取和释放顺序,以及避免持有多个锁的情况。
4. **分离和等待线程:** 对于不再需要等待的线程,可以将其设置为分离状态,避免资源的显式释放。而对于需要等待的线程,使用 `pthread_join` 函数来等待线程的结束,并获取其终止状态。
5. **栈大小设置:** 线程栈的大小是有限的,如果线程的栈空间不足,可能导致栈溢出。可以通过线程属性设置函数设置线程栈的大小,以适应线程的需要。
6. **错误处理:** 在使用线程的过程中,可能会出现错误,例如线程创建失败、线程同步错误等。需要适当地处理这些错误,确保线程能够正常运行,并及时通知相关的错误信息。
7. **线程安全函数:** 在多线程环境中,应尽量使用线程安全的库函数和数据结构,避免不必要的竞争和错误。
8. **线程优先级:** 线程优先级可以影响线程的调度顺序,但在使用时应谨慎,避免过度依赖线程优先级来实现程序逻辑,以免引发优先级反转等问题。
9. **线程取消:** 在需要取消线程时,应该谨慎使用线程取消功能,并确保线程在取消点上响应取消请求,并进行必要的清理工作。

 

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值