互斥锁(mutex) ---- 静态锁与动态锁
在多线程编程中,互斥锁(mutex)是确保共享资源安全访问的重要机制。POSIX线程(pthread)提供了两种方式来初始化互斥锁:静态初始化和动态初始化。理解这两种方式的区别和适用场景,对于编写高效、安全的多线程程序至关重要。
目录
一、静态初始化互斥锁
1. 定义与初始化
2. 优点
3. 缺点
4. 适用场景
5. 示例代码
二、动态初始化互斥锁
1. 定义与初始化
2. 优点
3. 缺点
4. 适用场景
5. 示例代码
三、选择合适的初始化方式
四、总结
五、题外话----pthread_mutex_init再探究
步骤 1:初始化属性对象
步骤 2:设置属性
步骤 3:动态初始化互斥锁
步骤 4:销毁属性对象
互斥锁的类型
1. 普通互斥锁(Default)
2. 递归互斥锁(Recursive)
3. 错误检查互斥锁(Error Checking)
4. 示例代码:使用递归互斥锁
总的来说
一、静态初始化互斥锁
1. 定义与初始化
静态初始化是通过宏PTHREAD_MUTEX_INITIALIZER
来完成的。例如:
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
这行代码声明并初始化了一个互斥锁my_mutex
。静态初始化的互斥锁在程序启动时就已经被初始化,不需要额外的函数调用。
2. 优点
- 简单易用:只需一行代码即可完成定义和初始化。
- 性能稍优:由于初始化是在编译时完成的,运行时开销较小。
3. 缺点
- 不可销毁:静态初始化的互斥锁不能通过
pthread_mutex_destroy
函数进行销毁。它们的生命周期与程序的生命周期一致。 - 灵活性有限:无法通过属性参数定制互斥锁的行为,例如设置递归锁或错误检查锁。
4. 适用场景
- 全局或静态变量:适用于那些在整个程序运行期间都需要存在的互斥锁。
- 简单程序:对于不需要动态管理互斥锁的简单程序,静态初始化是一个方便的选择。
5. 示例代码
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void *thread_increment(void *arg)
{
for (int i = 0; i < 1000; i++)
{
pthread_mutex_lock(&counter_mutex);
counter++;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
int main()
{
pthread_t threads[10];
for (int i = 0; i < 10; i++)
{
pthread_create(&threads[i], NULL, thread_increment, NULL);
}
for (int i = 0; i < 10; i++)
{
pthread_join(threads[i], NULL);
}
printf("Final counter: %d\n", counter);
return 0;
}
在这个示例中,静态初始化的互斥锁counter_mutex
被用来保护对共享计数器counter
的递增操作。程序运行后,最终的计数器值是正确的,说明静态初始化的互斥锁有效地保护了共享资源。
二、动态初始化互斥锁
1. 定义与初始化
动态初始化是通过调用pthread_mutex_init
函数来完成的。例如:
pthread_mutex_t my_mutex; pthread_mutex_init(&my_mutex, NULL);
这行代码首先声明了一个互斥锁my_mutex
,然后通过pthread_mutex_init
函数对其进行初始化。NULL
作为属性参数,表示使用默认的互斥锁属性。
2. 优点
- 可销毁:动态初始化的互斥锁可以通过
pthread_mutex_destroy
函数进行销毁,释放相关资源。 - 灵活性高:可以通过属性参数定制互斥锁的行为,例如设置递归锁或错误检查锁。
3. 缺点
- 初始化开销:需要在运行时调用
pthread_mutex_init
函数进行初始化,增加了运行时开销。 - 复杂性较高:需要额外的代码来管理互斥锁的生命周期。
4. 适用场景
- 动态分配的资源:适用于那些需要在运行时创建和销毁的互斥锁。
- 需要定制互斥锁行为的场景:例如,需要使用递归锁或错误检查锁时。
5. 示例代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
pthread_mutex_t lock;
int *array;
int size;
} Array;
void initialize_array(Array *arr, int size)
{
arr->array = malloc(size * sizeof(int));
arr->size = size;
pthread_mutex_init(&arr->lock, NULL);
}
void destroy_array(Array *arr)
{
pthread_mutex_destroy(&arr->lock);
free(arr->array);
}
void *thread_increment(void *arg)
{
Array *arr = (Array *)arg;
for (int i = 0; i < arr->size; i++)
{
pthread_mutex_lock(&arr->lock);
arr->array[i]++;
pthread_mutex_unlock(&arr->lock);
}
return NULL;
}
int main()
{
Array my_array;
initialize_array(&my_array, 100);
pthread_t threads[10];
for (int i = 0; i < 10; i++)
{
pthread_create(&threads[i], NULL, thread_increment, &my_array);
}
for (int i = 0; i < 10; i++)
{
pthread_join(threads[i], NULL);
}
for (int i = 0; i < my_array.size; i++)
{
printf("Array[%d] = %d\n", i, my_array.array[i]);
}
destroy_array(&my_array);
return 0;
}
在这个示例中,动态初始化的互斥锁arr->lock
被用来保护对动态分配的数组arr->array
的访问。程序运行后,数组中的每个元素都被正确递增,说明动态初始化的互斥锁同样有效地保护了共享资源。
三、选择合适的初始化方式
在实际应用中,选择使用静态还是动态初始化的互斥锁,需要根据具体的场景和需求来决定:
- 静态初始化:适用于全局或静态变量的初始化,使用简单且性能稍优。但不可销毁,灵活性有限。
- 动态初始化:适用于需要动态管理互斥锁生命周期,或需要定制互斥锁行为的场景。虽然初始化开销稍高,但提供了更大的灵活性和控制能力。
四、总结
静态初始化和动态初始化互斥锁是POSIX线程提供的两种不同的初始化方式,各有优缺点。理解它们的区别和适用场景,有助于编写更高效、更安全的多线程程序。
- 静态初始化:简单易用,适用于全局或静态变量。
- 动态初始化:灵活可销毁,适用于动态管理资源的场景。
根据实际需求选择合适的初始化方式,可以更好地利用互斥锁来保护共享资源,避免竞态条件和数据损坏。
五、题外话----pthread_mutex_init再探究
在动态初始化互斥锁时,pthread_mutex_init
函数的第二个参数attr
是一个指向pthread_mutexattr_t
结构体的指针。该结构体用于设置互斥锁的属性。NULL
表示使用默认属性,但也可以传入一个已初始化的pthread_mutexattr_t
指针,以定制互斥锁的行为。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
首先,pthread_mutexattr_t
是一个结构体,它包含了互斥锁的各种属性。这些属性包括:
- 类型(Type):决定互斥锁是普通锁、递归锁还是错误检查锁。
- 协议(Protocol):用于实现互斥锁的协议。
- 继承(Inheritance):决定子线程是否继承父线程的互斥锁属性。
接下来,我需要解释如何创建和设置一个pthread_mutexattr_t
对象。步骤如下:
- 初始化属性对象:使用
pthread_mutexattr_init
函数初始化一个属性对象。 - 设置属性:使用相应的函数(如
pthread_mutexattr_settype
)来设置互斥锁的类型。 - 动态初始化互斥锁:将设置好的属性对象传入
pthread_mutex_init
函数。 - 销毁属性对象:使用
pthread_mutexattr_destroy
函数销毁属性对象。
要使用自定义属性初始化互斥锁,需要按照以下步骤进行:
步骤 1:初始化属性对象
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
这行代码初始化了一个互斥锁属性对象attr
,使其具有默认属性。
步骤 2:设置属性
根据需要,调用相应的函数设置属性。例如,设置互斥锁为递归锁:
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
步骤 3:动态初始化互斥锁
将设置好的属性对象传入pthread_mutex_init
函数:
pthread_mutex_t my_mutex;
pthread_mutex_init(&my_mutex, &attr);
步骤 4:销毁属性对象
在不再需要属性对象时,调用以下函数销毁它:
pthread_mutexattr_destroy(&attr);
互斥锁的类型
通过pthread_mutexattr_settype
函数,可以设置互斥锁的类型。以下是常见的类型:
1. 普通互斥锁(Default)
- 默认类型:
PTHREAD_MUTEX_DEFAULT
- 行为:非递归锁。同一个线程不能多次锁定同一个互斥锁,否则可能导致死锁。
- 适用场景:大多数情况下,普通互斥锁已经足够。
2. 递归互斥锁(Recursive)
- 类型:
PTHREAD_MUTEX_RECURSIVE
- 行为:允许同一个线程多次锁定同一个互斥锁。每次加锁必须对应一次解锁,线程退出时自动释放所有锁。
- 适用场景:当一个线程需要多次访问受保护的资源时,递归互斥锁非常有用。
3. 错误检查互斥锁(Error Checking)
- 类型:
PTHREAD_MUTEX_ERRORCHECK
- 行为:如果一个线程尝试递归锁定一个普通互斥锁,或者在解锁时互斥锁未被锁定,会返回错误码
EDEADLK
。 - 适用场景:需要严格检查互斥锁使用错误的场景。
4. 示例代码:使用递归互斥锁
以下是一个使用递归互斥锁的示例:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t lock;
pthread_mutexattr_t attr;
void *thread_function(void *arg)
{
int *count = (int *)arg;
for (int i = 0; i < *count; i++)
{
pthread_mutex_lock(&lock);
printf("Thread %ld: Count = %d\n", pthread_self(), i);
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main()
{
// 初始化属性对象
pthread_mutexattr_init(&attr);
// 设置为递归互斥锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化互斥锁
pthread_mutex_init(&lock, &attr);
// 创建线程
pthread_t thread1, thread2;
int count = 5;
pthread_create(&thread1, NULL, thread_function, &count);
pthread_create(&thread2, NULL, thread_function, &count);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&lock);
// 销毁属性对象
pthread_mutexattr_destroy(&attr);
return 0;
}
在这个示例中,我们创建了一个递归互斥锁,允许同一个线程多次锁定和解锁。两个线程交替访问共享资源,确保输出有序。
总的来说
pthread_mutex_init
函数的第二个参数attr
可以传入NULL
或一个已初始化的pthread_mutexattr_t
指针。- 使用
NULL
表示使用默认属性(普通互斥锁)。 - 通过设置
attr
可以定制互斥锁的行为,例如使用递归锁或错误检查锁。 - 动态初始化互斥锁提供了更大的灵活性,适用于需要动态管理资源或定制行为的场景。