目录
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线程库接口声明:
-
线程创建与退出:
-
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
:用于创建一个新线程。 -
void pthread_exit(void *retval);
:用于终止调用线程,并返回一个指向某个对象的指针。
-
-
线程等待与同步:
-
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);
:用于锁定和解锁互斥量,实现线程间的同步。
-
-
线程属性与标识:
-
pthread_t pthread_self(void);
:用于获取调用线程的线程标识符。 -
int pthread_detach(pthread_t thread);
:用于将线程分离,使得线程在终止时自动释放其所有资源。
-
-
线程取消与比较:
-
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);
下面是关于这个函数的各个参数和返回值的详细解释:
参数
-
**
pthread_t *thread
**:-
这是一个指向
pthread_t
类型变量的指针,用于存储新创建线程的标识符。当pthread_create
成功创建一个线程后,新线程的线程ID会被存储在这个指针所指向的位置。
-
-
**
const pthread_attr_t *attr
**:-
这是一个指向
pthread_attr_t
类型变量的指针,用于指定线程的属性。这个参数可以为NULL
,表示使用默认的线程属性。线程属性可以包括线程栈的大小、调度策略、优先级等。
-
-
**
void *(*start_routine) (void *)
**:-
这是一个函数指针,指向新线程开始执行时调用的函数(即线程任务函数)。这个函数接受一个
void*
类型的参数,并返回一个void*
类型的值。通常,线程任务函数会执行线程需要完成的工作。
-
-
**
void *arg
**:-
这是一个指向任意类型的指针,作为参数传递给线程任务函数。这个参数可以为
NULL
,表示不传递任何参数给线程任务函数。
-
返回值
-
pthread_create
函数成功时返回0,失败时返回错误码。这些错误码在<errno.h>
头文件中定义,并可以通过perror
或strerror
函数转换为人类可读的字符串。
使用示例
下面是一个简单的 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; // 线程IDint 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
类型来表示的。
进程地址空间布局
进程地址空间是指一个进程在虚拟内存中的映射。每个进程都有自己独立的地址空间,这使得不同进程之间的数据是隔离的,从而提高了系统的安全性和稳定性。
进程地址空间通常包括以下几个部分:
-
文本段(Text Segment):也称为代码段,存储程序的二进制代码,即CPU执行的机器指令。这部分是只读的,防止程序意外地修改了它的指令。
-
数据段(Data Segment):存储程序中已初始化的全局变量和静态变量。这部分在程序加载时会被初始化,并且在程序执行期间可以修改。
-
BSS段:存储程序中未初始化的全局变量和静态变量。BSS段在程序加载时不占用实际的磁盘空间,只占用内存空间。
-
堆(Heap):动态内存分配的区域,由程序员在运行时通过如
malloc
、new
等函数进行分配和释放。 -
栈(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;}// 在主线程中获取并打印线程IDpthread_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_cancel
、pthread_setcancelstate
、pthread_setcanceltype
和 pthread_testcancel
是线程取消的例外,它们在执行时不会响应取消请求。
最后,主线程使用 pthread_join
等待被取消的线程结束,并检查线程的退出状态。如果被取消,pthread_join
的第二个参数将设置为 PTHREAD_CANCELED
。
pthread_exit函数
pthread_exit
函数的原型如下:
void pthread_exit(void *retval);
这里,retval
是一个指向某个类型的指针,它允许线程返回一个指向某种类型数据的指针,这个返回值可以通过pthread_join
函数来获取。如果线程没有返回值,或者不需要返回值,可以传递NULL
给pthread_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_push
和pthread_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
通常用于以下场景:
-
收集线程的返回值:如果线程函数有返回值,并且主线程或其他线程需要这个返回值,那么可以使用
pthread_join
来获取它。 -
确保线程完成其任务:有时,主线程或其他线程需要等待某个线程完成其特定的任务或资源释放操作,以确保数据的一致性或避免资源竞争。
-
线程同步:在某些复杂的并发场景中,线程之间的同步是必需的。
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)是一种特殊的线程设置,它决定了线程的生命周期是否与主线程的生命周期独立。当一个线程被设置为分离线程时,其资源在退出时会被自动回收,而不需要主线程等待其结束并调用线程清理函数。这种机制有助于简化线程管理,提高程序的性能和效率。
分离线程具有以下几个关键特性和优势:
-
资源自动回收:分离线程在退出时能够自动回收其占用的系统资源,包括内存和线程描述符等。这避免了资源泄漏和内存泄漏的风险,因为主线程无需显式地等待和回收分离线程的资源。
-
提高性能和效率:由于分离线程的自动回收机制,主线程无需等待分离线程结束,从而减少了主线程的等待时间。这使得主线程可以继续执行其他任务,而不会被阻塞在等待分离线程结束的操作上,从而提高了程序的性能和效率。
-
简化代码和逻辑:使用分离线程可以简化线程编程的代码和逻辑。程序员无需关注和处理线程的退出和资源回收,这使得编写简洁、清晰和可维护的多线程代码变得更加容易。
在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中的线程互斥
一、互斥的概念与重要性
互斥是一种同步机制,用于确保在某一时刻只有一个线程可以访问共享的数据资源。在多线程编程中,互斥锁扮演着至关重要的角色,它有助于防止数据不一致和其他并发问题。
二、互斥锁的实现与工作原理
-
操作系统与硬件支持
互斥锁的实现通常涉及到底层的操作系统支持和硬件支持。
-
锁的获取与释放
当一个线程尝试获取互斥锁时,如果锁已被其他线程持有,则该线程将被阻塞。一旦线程获取了互斥锁,它就可以安全地访问共享资源,并在完成访问后释放锁。
三、互斥锁的使用注意事项
-
避免死锁
死锁是指两个或更多线程在等待对方释放资源的情况,导致它们都无法继续执行。为了避免死锁,程序员需要仔细设计线程间的交互和同步机制。
-
防止饥饿
饥饿是指某些线程可能长时间无法获取到锁,导致它们无法完成其任务。为了避免饥饿,需要确保锁的获取和释放操作公平且合理。
四、总结
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
,从而避免了数据竞争和不一致的问题。
注意,在编写多线程程序时,还需要考虑其他同步机制(如条件变量、读写锁等)以及线程间的通信方式,以确保程序的正确性和性能。同时,也需要注意处理线程创建、同步和销毁时的错误情况,以避免程序崩溃或资源泄漏。
线程安全和可重入
线程安全
线程安全是多线程编程中的一个重要概念。在多线程运行的环境中,不论线程的调度顺序如何,如果最终的结果都是一致且正确的,那么这些线程就被认为是线程安全的。线程安全主要涉及到两个方面:
-
线程同步:确保同一时刻只有一个线程访问临界资源。这通常通过互斥锁、条件变量等同步机制来实现,以避免多个线程同时修改同一数据导致的数据不一致问题。
-
使用线程安全的函数:线程安全的函数指的是那些可以被多个线程同时调用而不会发生竞态条件的函数。竞态条件是指两个或多个线程在访问共享资源时,由于它们的执行顺序不同而导致结果不一致的情况。
造成线程不安全的原因通常是因为两个或更多的线程对象共享同一个资源,并且这些线程在没有适当同步的情况下操作这些共享资源。例如,全局变量在同一进程中的多个线程之间是共享的,如果多个线程在没有锁保护的情况下同时修改全局变量,就可能导致数据不一致。
可重入函数
可重入函数与线程安全有着密切的关系。一个函数被称为可重入的,如果它可以在任何时候被中断,然后在稍后的某个时刻从中断点继续执行,而不会丢失数据或产生不正确的结果。换句话说,可重入函数具有无状态性,即其执行不依赖于先前的函数调用或全局状态。
常见的不可重入的情况包括:
-
调用了malloc/free函数,因为malloc函数是使用全局链表来管理堆的,可能会被其他线程打断。
-
调用了标准I/O库函数,标准I/O库的很多实现都是以不可重入的方式使用的。
-
使用了全局变量或静态变量,并且这些变量的状态在函数执行过程中会发生变化。
如果一个函数是线程安全的,那么它通常也是可重入的,因为线程安全要求函数在多线程环境中能够正确运行,而不会导致数据竞争或其他并发问题。反之,如果一个函数是不可重入的,那么它就不能由多个线程同时使用,否则可能会引发线程安全问题。
总结来说,线程安全和可重入性是多线程编程中需要关注的重要概念。确保线程安全和函数的可重入性有助于编写出稳定、可靠的多线程程序。
常见锁的概念
在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_datapthread_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系统中,死锁通常发生在多线程或多进程环境中,当它们试图同时访问共享资源时,可能由于资源的竞争和不当的同步机制而陷入死锁状态。死锁的发生需要满足以下四个必要条件:
-
互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只有一个进程可以使用。如果其他进程请求该资源,则请求者只能等待,直到该资源被当前占有者释放。
-
持有并等待条件(Hold and Wait):一个进程至少已经持有一个资源,并正在等待获取一个当前被其他进程持有的资源。
-
非抢占条件(No Preemption):资源只能被占有它的进程显式地释放,资源不能被抢占。
-
循环等待条件(Circular Wait):存在一个进程-资源的循环等待链,其中每个进程都在等待下一个进程所持有的资源。
如何避免死锁
为了避免死锁,可以采取以下策略:
-
预防死锁:通过破坏死锁的四个必要条件中的一个或多个来预防死锁。
-
避免死锁:通过银行家算法等策略,在运行时动态地判断是否分配资源,从而避免系统进入不安全状态。
-
检测和解决死锁:通过定期检测死锁的发生,一旦检测到死锁,则通过抢占资源或终止进程等方法来解决死锁。
避免死锁的算法
银行家算法是一种经典的避免死锁的算法,它模拟银行家借贷的场景,在分配资源之前,判断分配后系统是否处于安全状态。如果是,则分配资源;否则,不分配资源,从而避免系统进入不安全状态。
避免死锁的示例代码
在实际编程中,避免死锁通常依赖于良好的编程习惯和同步机制的选择。以下是一个简单的示例,展示如何在多线程环境中避免死锁:
#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先获取mutex1printf("Thread 1 acquired mutex 1\n");sleep(1); // 模拟一些工作pthread_mutex_lock(&mutex2); // 然后尝试获取mutex2printf("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先获取mutex2printf("Thread 2 acquired mutex 2\n");sleep(1); // 模拟一些工作pthread_mutex_lock(&mutex1); // 然后尝试获取mutex1printf("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;
}
在这个例子中,两个线程分别尝试按照不同的顺序获取两个互斥锁。由于每个线程都先获取一个锁,然后尝试获取另一个锁,因此不会发生死锁。这是因为每个线程都在等待一个已经被另一个线程持有的锁,但没有形成循环等待链。
当然,在更复杂的场景中,避免死锁可能需要更复杂的策略,包括设计良好的同步协议、使用条件变量来避免忙等待、确保锁的粒度合适,以及适当地使用超时和重试机制等。