2026-3-22 Pthread多线程编程

1 Pthread概述
 1.1 什么是POSIX?Portable Operating System Interface of UNIX, POSIX,表示可移植操作系统接口。POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在UNIX操作系统上运行的软件而定义的一系列API标准的总称。
 1.2 什么是Pthread?历史上,硬件销售商实现了私有版本的多线程库,这些实现在本质上各自不同,使得程序员很难开发可以移植的应用程序。为了使用线程所提供的强大优点,需要一个标准的程序接口。对于UNIX系统,IEEE POSIX 1003.1c标准制定了这一标准接口,依赖于该标准的实现就称为POSIX threads或者Pthreads。Pthread不是编程语言,而是定义了处理线程的一系列C语言类型的API。
 总结一下就是,Pthreads是程序员控制操作系统线程功能的遥控器,操作系统负责创建,调度,销毁线程,程序员通过Pthread编写程序告诉操作系统,我要创建几个线程,每个线程执行哪些任务。线程是操作系统创建的,但是每个线程执行什么代码,处理什么数据,是程序员指定的,也就是后面传入的函数。
image
这一页的重点就是,同一个进程中的所有线程共享同样的地址空间,这是共享内存模型的基础,因为共享内存,所以线程之间可以直接传递数据,传指针就可以,但是正因为共享内存,需要用到互斥量来保护临界区。
 1.3 Pthread应用于共享内存编程模型:把数据/任务拆成独立块,多个块同时处理各自的块,必要时同步。也就是将任务拆解成离散的,独立的,可以并发执行的程序,然后在这门课的重点就是数据并行,也就是按照数据拆成独立块。
 1.4 再论共享内存编程模型
image
 所有线程访问全局共享内存,意思是大数组,全局变量,所有线程都可以用;线程有私有数据,意思是局部变量,函数参数互不干扰;程序员负责同步保护,意思是多个线程在写同一个数据的时候会发生冲突,需要加锁,这门课常用到的就是互斥量实现。这其实规定了Pthread程序的框架,首先是定义共享的数据,也就是大家都能访问的数据;其次是定义线程的私有数据,也就是各个线程自己想要实现的内容;最后是对于每个线程都需要修改的共享数据进行加锁保护修改,避免冲突;
 1.5 Pthread API
image
image
image
这几页就是在说具体的Pthread里面的函数可以分成哪几类,重点就是线程管理类的和互斥量类的。
image
可以理解一下命名规范。
2 线程管理
 2.0 前置知识
 关于变量在内存中的存储:从高地址到低地址,依次是栈区(Stack),堆区(Heap),全局/静态区和代码区。栈区存放局部变量、函数参数,自动分配,函数结束时自动释放;堆区是需要通过malloc()函数申请的内存区域,需要手动申请,手动释放;全局/静态区存放全局变量和static定义的静态变量,在程序启动时分配,结束时释放;代码区则存放函数代码。小问题:main()函数里面定义的变量算什么,存放在哪个区?解答:main()相当于一个特殊的函数,所以里面的变量还是在栈区,结束了就自动释放,但是里面的变量可以指向堆区的变量,这个堆区的变量需要手动释放。

┌─────────────────────┐  高地址
│       栈区          │  ← 局部变量、函数参数
│   (Stack)           │     自动分配,函数结束自动释放
├─────────────────────┤
│       堆区          │  ← malloc申请的内存
│   (Heap)            │     手动申请,手动释放
├─────────────────────┤
│    全局/静态区       │  ← 全局变量、static变量
│                     │     程序启动时分配,结束时释放
├─────────────────────┤
│    代码区           │  ← 程序代码
│                     │
└─────────────────────┘  低地址

 关于&和*:&叫做取地址运算符,作用是获取变量在内存中的地址。

int a = 10;      // a是int变量,值是10
int* p = &a;     // &a是a的地址,p是指针变量,存的是a的地址

// 图示:
// 内存地址:0x1000    0x2000
//          ┌─────┐   ┌────────┐
//          │ 10  │   │ 0x1000 │  ← p的值是a的地址
//          │  a  │   │   p    │
//          └─────┘   └────────┘
//           &a=0x1000  &p=0x2000

则有两个完全不同的含义,1)在声明时表示是这个变量是一个指针,比如int p表示p是一个指向int的指针;2)在使用时表示解引用,取这个指针所执行的地址里面的变量的那个值,比如*p=20表示把p指向的地址里面存放的值变成20。

int a = 10;
int* p = &a;     // *在声明:p是指针

*p = 20;         // *在使用:解引用,把a改成20
printf("%d", a); // 输出20

 指针就是地址加上类型,需要注意的是指针也有自己的地址。

int a = 10;
int* p = &a;

// p里面存的是什么?—— 地址(比如0x1000)
// p的类型是什么?—— int*(指向int的指针)
// *p是什么?—— 该地址上的int值(10)
printf("%p", &a);  // a的地址,比如0x1000
printf("%p", p);   // p的值,也是0x1000(和上面一样)
printf("%p", &p);  // p自己的地址,比如0x2000

 关于指针在函数前面里面的使用,只需要知道void func(int* p)和void func(int *p)是一样的,都是声明,表示需要传一个指向int类型的指针,也就是地址,所以使用的时候需要&取地址。

// ========== 场景1:在函数参数里(声明)==========
void func(int* p);      // 这是声明:p是指向int的指针
// 等价于
void func(int *p);      // *靠近类型或变量都一样

// 调用时(使用)
int a = 10;
func(&a);               // &是取地址,传入a的地址
// 或者
int* p = &a;
func(p);                // p已经是地址,直接传


// ========== 场景2:在函数体里(使用)==========
void func(int* p) {     // p是地址(指针变量)
    *p = 20;            // *p是解引用:把p指向的地址设为20
}

 关于出参的概念:出参就是指通过指针,让函数修改调用者的变量。

// 方式1:传值(值传递)—— 只进不出
void func1(int x) {
    x = 100;  // 修改的是副本,外面不变
}

int a = 10;
func1(a);     // a还是10

// 方式2:传指针(地址传递)—— 可以出
void func2(int* x) {   // 参数是指针(地址)
    *x = 100;          // 解引用,修改地址上的值
}

int a = 10;
func2(&a);    // 传a的地址,a变成100
// 经典例子:swap函数
void swap(int* a, int* b) {  // 必须传地址才能"出"
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int x = 10, y = 20;
swap(&x, &y);  // x和y真的交换了

 关于栈和堆在使用中的介绍:首先栈的特点是,自动管理变量,自动分配,自动释放,大小有限,通常就几MB,在函数结束的时候,栈上的变量会消失。

void func() {
    int a = 10;      // 栈上分配
    int arr[100];    // 栈上分配
    
    // 函数结束时,a和arr自动释放
}

 也就是说,不能让函数返回函数内部定义的变量的地址,因为在函数结束的时候,该变量是在栈上的会被自动释放。

int* bad_func() {
    int x = 10;
    return &x;  // ❌ 返回局部变量地址!函数结束x已销毁
}

 堆的特点是,里面的变量都是需要自己手动分配(通过malloc()函数),手动释放的(通过free()函数),大小只受限于物理内存,因为你自己指定想多大就多大,由于是手动的,所以变量的整个生命周期由程序员控制。与栈不同的是,由于在函数内部分配的堆空间不会自动销毁,所以在函数内部的堆变量是可以返回的。

int* good_func() {
    int* p = malloc(sizeof(int));  // 意思是分配一个大小为int类型大小的堆空间,并用一个指针p指向这个空间
    *p = 10;
    return p;  // ✓ 堆内存不会自动释放
}

 2.1 线程的创建和终止
 想要使用Pthread,首先需要导入库,也就是通过#include<pthread.h>来使用Pthread。下面我们详细了解四个创建和终止线程的函数。
  pthread_create():创建线程,最重要的函数,先看他的函数签名。

int pthread_create(
    pthread_t *thread,           // 参数1:pthread_t指针(出参)
    const pthread_attr_t *attr,  // 参数2:属性指针(常为NULL)
    void *(*start_routine)(void*), // 参数3:函数指针
    void *arg                    // 参数4:void指针(传参)
);

  第一个参数:pthread_t *thread,这是声明,thread是指向pthread_t类型的一个指针,也就是说,使用的时候需要传入地址,而这个地址的类型是pthread_t,是一个不透明类型,不同系统实现不同,只需要知道有这么一个类型就好了。实际运行的流程是,在外部定义一个pthread_t类型的变量以后,将这个变量取地址&p(因为指针就是有类型的地址),传入pthread_create()参数,这是一个出参,也就是说,调用之后在函数内部,操作系统会给这个线程一个特定的ID,需要写回给外部定义的pthread_t类型的变量,作为这个线程的唯一标识符。不过这个标识符是操作系统定义的,很复杂,后面写代码还需要自己给线程一个id。

pthread_t *thread
//      ↑
//      └── 这是声明:thread是指向pthread_t的指针

// 调用时:
pthread_t tid;
pthread_create(&tid, ...);  
//             ↑
//             └── &是取地址,传入tid的地址

  第二个参数:const pthread_attr_t *attr,这也是声明,后面表示attr是一个指向pthread_attr_t类型的指针,前面的const表示操作系统不会(或者说不能)修改这个指针指向的内容。这里不会过多涉及,实际使用的时候传NULL即可。

const pthread_attr_t *attr
//                    ↑
//                    └── 声明:attr是指向pthread_attr_t的指针
// const表示OS不会修改这个指针指向的内容

// 调用时:
pthread_create(..., NULL, ...);   // 传NULL(空指针)
// 或者
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_create(..., &attr, ...);  // 传attr的地址

  第三个参数:void (start_routine)(void),这个是一个函数指针,可以简单理解为指向一个函数的指针。既然是函数,就有返回值和函数的参数,因此前面的void和后面的void,全部都表示这个函数start_routine的参数和返回值都是void,而start_rountine就表示这是一个指向一个函数的指针。这就解释了两个问题,为什么编写给每一个线程的函数的时候,都规定函数的返回值和参数必须是void,并且在内部进行类型转换,这是由pthread_create()的函数签名规定的;第二个问题是使用的时候直接传定义的函数名称即可,内部会自动处理,不需要深究。

void *  (  *start_routine  )  (void*)
│        │      │            │
│        │      │            └── 参数:接收void*类型
│        │      │
│        │      └── 变量名:start_routine(函数指针的名字)
│        │
│        └── *:表示这是一个指针
│
└── 返回值:返回void*类型
// pthread_create需要知道:创建线程后执行哪段代码
// 所以你要传一个"函数地址"给它

void* my_thread(void* arg) {   // 你的线程函数
    ...
}

pthread_create(..., my_thread, ...);
//                   ↑
//                   └── 函数名就是地址(隐式转函数指针)

  第四个参数:void *arg,这是一个声明,表示一个指向任意类型的指针,因此使用的时候也需要传地址。在线程函数里面,会将任意类型指针转换成具体类型。

void *arg
//     ↑
//     └── 声明:arg是指向任意类型的指针(void*是通用指针)

// 调用时:
int value = 42;
pthread_create(..., &value);      // 传int*,自动转void*
// 或者
double d = 3.14;
pthread_create(..., &d);          // 传double*,自动转void*
// 或者
pthread_create(..., NULL);        // 不传参数

  总结:

// ========== 声明(函数签名)==========
int pthread_create(
    pthread_t *thread,           // 声明:需要传pthread_t的地址
    const pthread_attr_t *attr,  // 声明:需要传attr的地址或NULL
    void *(*start_routine)(void*), // 声明:需要传函数地址
    void *arg                    // 声明:需要传任意数据的地址
);

// ========== 调用(实际使用)==========
pthread_t tid;                    // 定义变量
int value = 42;

pthread_create(
    &tid,                         // 使用:取tid的地址(符合pthread_t*)
    NULL,                         // 使用:空指针(符合pthread_attr_t*)
    my_thread,                    // 使用:函数名即地址(符合函数指针)
    &value                        // 使用:取value的地址(符合void*)
);

  其实第四个参数和第三个参数是配套使用的,分别表示传给线程的数据,和传给线程的函数:

pthread_create(&tid, NULL, my_thread, &value);
//                           ↑         ↑
//                           │         │
//                           │         └── 第4参数:arg(传给线程的数据)
//                           │
//                           └── 第3参数:start_routine(线程函数)

// 内部发生:
// OS创建线程 → 调用 my_thread(&value)
//                         ↑
//                         └── 第4参数作为第3参数函数的实参

  比如下面这个例子:

// ========== 主线程 ==========
int main() {
    pthread_t tid;
    int value = 42;                    // 要传的数据
    
    pthread_create(&tid, NULL, 
                   my_thread,          // 第3参数:函数名
                   &value);            // 第4参数:数据的地址
    
    // 内部等价于:在新线程里执行 my_thread(&value)
}


// ========== 线程函数 ==========
void* my_thread(void* arg) {           // arg接收的就是&value
    int* p = (int*)arg;                // p指向value,这个地方就是前面说的转成具体的类型,首先将arg强制转换成int*,也就是指向int类型的指针,然后用一个新的指针变量,也是指向int类型的指针(这里是声明)指向这个地址
    printf("%d\n", *p);                // 输出42,这里是解引用
    pthread_exit(NULL);
}

  关于类型转换,最后解释一下:

void* my_thread(void* arg) {    // arg = &value (void*类型,值为0x1000)
    int* p = (int*)arg;         // (int*)arg = 还是0x1000,但类型变成int*
                                // p = 0x1000,类型是int*
    
    printf("%d\n", *p);         // *p = 解引用0x1000,得到42
}
内存地址:0x1000      0x2000      0x3000
         ┌─────┐    ┌─────┐    ┌─────┐
         │  42 │    │ arg │    │  p  │
         │value│    │0x100│    │0x100│
         └─────┘    │0(void*)│   │0(int*)│
                    └─────┘    └─────┘
                    
         ↑________________↑____________↑
              指向同一地址
              只是类型不同
void* arg;
*arg = 20;        // ❌ 编译错误!void*不能解引用

// 必须先转换
*(int*)arg = 20;  // ✓ 先转int*,再解引用
// 或者
int* p = (int*)arg;
*p = 20;          // ✓ 用p解引用

  pthread_exit():线程退出,这个是用在自己定义的函数的最后,表示这个线程需要返回的东西。还是一样的首先理解函数签名:

void pthread_exit(void *status);   // 参数:退出状态指针

  这里的参数就是一个表示退出状态的指针,这里又用到了void*,说明这个指针在使用的时候,要么是NULL不返回结果,要么是返回结果了,在main()里面要进行类型转换才可以解引用得到值。

// 第一种情况
void* thread_func(void* arg) {
    // ... 干活 ...
    pthread_exit(NULL);   // 直接传NULL
}

  注意如果要返回,必须给结果分配堆内存,否则一个线程结束,类似一个函数结束,内部的变量是在栈上的,会被回收,无法返回,所以必须分配内存,然后返回。

// 第二种情况
void* thread_func(void* arg) {
    int* result = malloc(sizeof(int));
    *result = 100;
    pthread_exit(result);  // 传堆地址
}
// 主线程使用结果
void* ret;                    // void* 接收返回值
pthread_join(tid, &ret);      // &ret是出参,OS把result写进来
int* p = (int*)ret;           // 转换回int*
printf("%d\n", *p);           // 输出100
free(p);                      // 记得释放

  pthread_attr_init():初始化属性函数,了解函数签名即可,不深入,直接传NULL。

int pthread_attr_init(pthread_attr_t *attr);   // 参数:属性对象地址

  pthread_attr_destory():销毁属性,不深入。

int pthread_attr_destroy(pthread_attr_t *attr);   // 参数:属性对象地址

 其他知识点:
image
 这一页主要讲整个多线程程序的流程

程序启动
    │
    ▼
OS创建主线程 ──→ 执行main()
                    │
                    ├── pthread_create() ──→ 创建子线程
                    │                           │
                    │                           ▼
                    │                       执行thread_func()
                    │                           │
                    ▼                           │
                继续执行main()                  │
                                                ▼
                                            pthread_exit()
                                                │
                                                ▼
                                            子线程结束

 四个重点:main是线程,在程序启动的时候就会有一个主线程;任何线程都可以通过pthread_create()创建线程,也就是随处可创建;通过这种方式创建的线程地位是平等的(peers),也就是不存在父子关系(对比进程,fork()创建的是有父进程和子进程的概念的);线程的数量是受内存数量的限制的,因为每个线程都有自己的运行栈,是有大小的。
image
 重点是创建关系不等于执行顺序,也就是说不能假设某个线程比另一个线程先运行完,写的程序也不能依赖于线程之间的执行顺序,如果需要用到顺序的内容,应该用同步机制(如后面讲的join和mutex)来控制顺序。

// ❌ 错误:假设t1先执行完
pthread_create(&t1, NULL, func1, NULL);
pthread_create(&t2, NULL, func2, NULL);
// 以为t1的结果t2能用?不一定!t2可能先跑完

// ✓ 正确:用join强制顺序
pthread_create(&t1, NULL, func1, NULL);
pthread_join(t1, NULL);        // 等待t1结束
pthread_create(&t2, NULL, func2, NULL);  // 再创建t2

image
 不深入,传NULL即可
image
 重点是线程的几种退出方式,pthread_exit()的作用就是线程自己退出;如果main()主线程也用了这个退出方式,其他线程将会自己执行;pthread_exit()的参数是线程的返回状态,可以被join函数接受。
 2.2 向线程传递参数
image
 重点是你要知道,pthread_create()只能传一个参数,如果你要传多个参数,你要用结构体打包。讲这个是因为后面要用数组来实现多线程,给每个线程一个ID,以及使用每个线程。所有参数都应该传递引用并转换成void*,这是由前面介绍的函数签名决定的。

// 打包多个参数
typedef struct {
    int id;
    double score;
    char name[20];
} ThreadArg;

void* func(void* arg) {
    ThreadArg* p = (ThreadArg*)arg;  // 解包
    printf("id=%d, score=%f\n", p->id, p->score);  // 这里是结构体指针访问变量的方式
    pthread_exit(NULL);
}

int main() {
    ThreadArg arg = {1, 98.5, "Alice"};
    pthread_create(&tid, NULL, func, &arg);  // 传结构体地址
}

 2.3 连接Join和分离detach
 我们依旧从函数入手,介绍几个重要的函数。
  pthread_join():连接是一种在线程间完成同步的方法,这个函数会阻塞调用的线程直到第一个参数指定的线程终止为止。它的函数签名为:

int pthread_join(
    pthread_t thread,     // 第1参数:线程ID(值,不是指针)
    void **status         // 第2参数:接收返回值的地址(出参)
);

  第一个参数:pthread_t thread,注意这里不是指针了,而直接是一个pthread_t类型。意思是这里不是出参,因为在前面的创建过程中已经给创建的线程由操作系统分配了一个唯一的线程ID,这里只是作为入参,通过ID找到这个线程而已。
  第二个参数:void **status,表示指针的指针,这个有点抽象,我们这样子理解:首先,在目标线程里面调用pthread_exit()函数,程序员可以在主线程中获得目标线程的终止状态。通过前面对exit()函数的介绍,我们知道这个函数的参数可以是NULL,或者是在每个线程中指定的堆变量。但是不管怎么样,我们可以理解为线程结束之后返回的是一个指向void的指针,因为pthread_exit(void *status)已经决定了,意思是线程结束,返回的是一个地址。这个地址会被操作系统保存,当主线程也就是main()调用pthread_join()函数时,会把这个地址写入第二个变量。注意这里为什么需要用指针的指针,因为通过exit()函数返回的,本就是一个指向void类型的指针(也就是我们说的地址),因此为了让主线程也可以出参这个变量,需要用一个指向void类型的指针的指针,来表示这个地址,否则无法出参。具体的流程为:

子线程结束
    │
    ▼
pthread_exit(void *status)  ← 参数是void*,即"地址"
    │
    └── 返回一个地址(NULL 或 堆变量地址)
              │
              ▼
         这个地址被OS保存
              │
              ▼
    pthread_join(tid, &ret)  ← 主线程来取
              │
              └── &ret是void**(因为我们会在外面定义void* ret),即"地址的地址"
                        │
                        └── OS把保存的地址,写到ret里
子线程:                     主线程:
   │                           │
   ▼                           │
malloc(sizeof(int)) ──→ 堆内存  │
   │                           │
*p = 42;                      │
   │                           │
pthread_exit(p);  ────────────┼──→ OS保存p的值(比如0x2000)
   │         (返回地址)      │
   │                           ▼
线程结束                    pthread_join(tid, &ret)
                               │
                               └── OS把0x2000写到ret
                               │
                               ▼
                            ret = 0x2000(void*)
                               │
                               ▼
                            *(int*)ret == 42  //第一个是强制把ret转变为int,然后解引用

  pthread_detach():分离线程,作用是把线程设置为分离状态,在线程结束时自动回收资源,并且分离之后不能再join。

int pthread_detach(pthread_t thread);   // 参数:线程ID

  pthread_attr_setdetachstate():设置分离属性,不用

int pthread_attr_setdetachstate(
    pthread_attr_t *attr,      // 属性对象地址
    int detachstate            // 分离状态
);

  pthread_attr_getdetachstate():获取分离属性,不用

int pthread_attr_getdetachstate(
    const pthread_attr_t *attr,   // 属性对象地址
    int *detachstate              // 出参,接收当前状态
);

image
  这里比较有意思。首先这里其实提到了两种同步,第一种同步是线程之间的同步,同步的是线程之间的生命周期,也就是join实现的同步;第二种同步是每个线程对数据访问的同步,也就是读写共享数据,需要加锁,写,解锁的过程,也就是通过互斥量和条件变量实现的同步。在实际的使用中,这两种同步方式是一起使用的:比如在创建和销毁线程,也就是main()里面,会用到join同步,需要等所有线程都执行完了,才结束主函数。而在每个线程内部执行的函数里面,会用到mutex同步,作用是每个线程执行完自己的操作,需要给全局变量处理的时候,需要通过互斥量来加锁,写,解锁。举例如下:

#include <pthread.h>

int global_sum = 0;           // 共享数据
pthread_mutex_t mutex;        // 保护共享数据的锁

void* worker(void* arg) {
    int id = *(int*)arg;
    int local = 0;
    
    // 每个线程计算自己的部分
    for (int i = 0; i < 100; i++) {
        local += i;
    }
    
    // 【互斥量同步】保护global_sum,防止同时写
    pthread_mutex_lock(&mutex);
    global_sum += local;      // 临界区:一次只有一个线程能执行
    pthread_mutex_unlock(&mutex);
    
    pthread_exit(NULL);
}

int main() {
    pthread_t tids[4];
    int ids[4] = {0, 1, 2, 3};
    
    pthread_mutex_init(&mutex, NULL);
    
    // 创建4个线程
    for (int i = 0; i < 4; i++) {
        pthread_create(&tids[i], NULL, worker, &ids[i]);
    }
    
    // 【Join同步】等待所有线程结束
    for (int i = 0; i < 4; i++) {
        pthread_join(tids[i], NULL);
    }
    
    printf("总和 = %d\n", global_sum);  // 现在可以安全读取
    
    pthread_mutex_destroy(&mutex);
    return 0;
}
主线程                    4个工作线程
   │                           │
   ├──create(t0)────────────→│
   ├──create(t1)────────────→│
   ├──create(t2)────────────→│
   ├──create(t3)────────────→│
   │                      各自计算local
   │                           │
   │                      lock→改global→unlock(互斥同步)
   │                           │
   │                      pthread_exit
   │                           │
   ├──join(t0)───────────────┘  ← 等t0结束(join同步)
   ├──join(t1)────────────────┘
   ├──join(t2)────────────────┘
   ├──join(t3)────────────────┘
   │
   ▼
打印global_sum(此时所有线程已结束,数据完整)

image
 重点是线程有可连接和分离两种状态:可连接是默认状态,可以等待,可以join,资源在join的时候回收;分离是独立运行的状态,不能join,资源会自动回收。
image
 较少涉及,了解即可。
3 互斥量
 3.1 互斥量概述
  这个概述感觉PPT的内容总结的已经很好了,都是精华。互斥量的使用就需要用到Pthread里面的关于互斥量的API,所以下面会继续介绍,主要包括互斥量的创建和销毁、互斥量的加锁和解锁。
image
image
 3.2 创建和销毁互斥量
  pthread_mutex_init():动态初始化互斥量,函数签名如下,下面的pthread_mutex_t其实就是一个互斥量类型,这样理解即可。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

  第一个参数:pthread_mutex_t *mutex,这是声明,表示mutex是一个指向pthread_mutex_t类型的指针,也就说明实际使用的时候要指向一个互斥量的地址。因为这是一个出参,跟前面的tid类似,我们在外部指定了一个存放该类型的地址,在调用初始化函数的时候把这段地址传给函数,函数就会把初始化的互斥量放到这个地址里面。
  第二个参数:const pthread_mutexattr_t *attr,这是声明,指向一个pthread_mutexattr_t类型的指针,是一个属性对象指针。前面的const表示在函数内部并不会修改这个指针的内容。和线程属性一样,互斥量也有属性,不过实际使用的时候不会深入,只需传NULL即可。
  整个函数成功时返回0,没有成功则返回非0值表示错误码。具体使用方式如下:

pthread_mutex_t mymutex;                    // 声明互斥量变量
pthread_mutex_init(&mymutex, NULL);         // 动态初始化,使用默认属性

  除了这种动态初始化互斥量方法,还可以采用静态初始化方法:

pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;

  这里的PTHREAD_MUTEX_INITIALIZAER是系统预定义的宏,属于Pthread标准定义的常量宏,存放在头文件当中,会给互斥量一个初始值。

┌────────────────────────────────────────┐
│  静态初始化(编译时搞定)                │
│                                        │
│  pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; │
│                                        │
│  ↓ 编译器直接填入默认值                  │
│                                        │
│  程序启动时,mymutex已经是有效互斥量      │
│  (但只能用默认配置)                    │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│  动态初始化(运行时函数调用)             │
│                                        │
│  pthread_mutex_t m;                    │
│  pthread_mutex_init(&m, NULL);  ← 运行时执行 │
│                                        │
│  ↓ 函数执行时才分配资源、设置状态          │
│                                        │
│  可以自定义属性,可以检查返回值错误        │
└────────────────────────────────────────┘

  pthread_mutex_destroy():销毁互斥量。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

  只有一个参数:pthread_mutex_t *mutex,这是声明,表示一个指向pthread_mutex_t类型的指针,也就是实际使用的时候只需要把这个类型的地址传给这个函数,函数内部就会帮我们销毁存放在这个地址上面的互斥量。

pthread_mutex_destroy(&mymutex);            // 用完记得销毁,释放资源

  需要注意的是,在销毁之前要确保没有线程对这个互斥量加锁,前面线程在初始化之后默认是没有加锁的。也就是说应该在所有线程都完成任务之后再释放,就不会出现错误。
  int pthread_mutexattr_init(pthread_mutexattr_t *attr):初始化属性对象。
  int pthread_mutexattr_destroy(pthread_mutexattr_t *attr):销毁属性对象。
image
image
 3.3 锁定和解锁互斥量
  pthread_mutex_lock():阻塞式锁定,函数签名如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

  只有一个参数pthread_mutex_t *mutex,这是一个声明,表示需要一个指向pthread_mutex_t类型的指针,也就是一个地址,因此在线程需要使用互斥量的时候,应该给他传经过初始化后的互斥量的地址。这种锁定方式称为阻塞式锁定,有以下三种情况:第一种是互斥量没有加锁,那么线程立马锁定成功,返回0;第二种情况是互斥量被其他不同的线程上锁,当前线程就会阻塞等待,也就是不去做其他的事情,阻塞了,直到其他线程解锁,然后当前线程参与竞争锁定互斥量;第三种情况是互斥量已经被本线程锁定,这就会导致死锁。

pthread_mutex_lock(&mutex);     // 申请锁,拿不到就等着
// ========== 临界区开始 ==========
// 访问共享数据
sum += local_sum;               // 例如:累加部分结果到全局变量
// ========== 临界区结束 ==========
pthread_mutex_unlock(&mutex);   // 释放锁

  pthread_mutex_trylock():非阻塞尝试锁定,函数签名如下:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

  同样的需要传地址。和阻塞式锁定不同的是,只会有两种状态,如果当前互斥量没有被加锁,那么当前线程就加锁使用互斥量执行临界区代码;如果当前互斥量被锁定,不在乎谁锁的,当前线程马上返回一个错误值,不会阻塞等待解锁,会去干别的事。

if (pthread_mutex_trylock(&mutex) == 0) {
    // 拿到锁了,执行临界区
    sum += local_sum;
    pthread_mutex_unlock(&mutex);
} else {
    // 没拿到锁,做其他事情(不等待)
    printf("锁被占用,先干别的...\n");
}

  pthread_mutex_unlock():解锁,函数签名如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

  比较简单,也是传地址。需要注意的是必须遵循谁加锁,谁解锁的原则,否则会错误。

#include <pthread.h>

pthread_mutex_t mutexsum = PTHREAD_MUTEX_INITIALIZER;  // 静态初始化
double sum = 0.0;                                       // 全局共享变量

void* Thread_sum(void* rank) {
    int my_rank = *(int*)rank;
    double my_sum = 0.0;                                // 局部变量(私有)
    
    // ... 计算 my_sum(各自并行计算,无需锁)...
    for (long long i = my_first_i; i < my_last_i; i++) {
        my_sum += 4.0 / (1.0 + x*x);                    // 各自算各自的
    }
    
    // 关键:合并结果时需要锁保护
    pthread_mutex_lock(&mutexsum);                      // 【锁定】
    sum += my_sum;                                      // 临界区:更新全局变量
    pthread_mutex_unlock(&mutexsum);                    // 【解锁】
    
    pthread_exit(NULL);
}
┌─────────────────────────────────────────┐
│  数据划分(并行计算阶段)                 │
│  Thread 0: 计算 i ∈ [0, N/4)            │
│  Thread 1: 计算 i ∈ [N/4, N/2)           │  ← 无锁,完全并行
│  Thread 2: 计算 i ∈ [N/2, 3N/4)          │
│  Thread 3: 计算 i ∈ [3N/4, N)            │
└─────────────────────────────────────────┘
                    ↓ 各自得到 my_sum
┌─────────────────────────────────────────┐
│  互斥量保护(合并结果阶段)               │
│  ┌─────────┐  ┌─────────┐              │
│  │ Lock    │  │ Lock    │              │
│  │ sum+=x  │  │ 等待... │  ← 串行合并   │
│  │ Unlock  │  │         │              │
│  └─────────┘  │ Lock    │              │
│               │ sum+=y  │              │
│               │ Unlock  │              │
│               └─────────┘              │
└─────────────────────────────────────────┘

image
  所有访问共享数据的线程,都必须全部使用互斥量,这是线程之间的君子协议;如果多个线程等待同一个锁,解锁后谁能拿到,这是完全随机的,由系统调度器决定,除非使用优先级调度机制。
4 条件变量(自学)
5 信号量(自学)
6 Pthread API参考(自学)
7 总结
image
image
image
image
image

posted @ 2026-03-22 10:25  yyyyhc0214  阅读(7)  评论(0)    收藏  举报