在现代操作系统中,进程和线程是两个基本的并发执行单元,它们在资源分配、调度和管理方面有所不同。以下是关于进程切换开销、轻量级进程(LWP)、线程共享地址空间以及Linux对进程和线程的处理的一些解释:
-
进程切换开销:
- 当操作系统在多个进程之间进行上下文切换时,需要保存当前进程的状态(如寄存器值、程序计数器等),加载新进程的状态,这涉及到大量的内存读写操作,因此开销较大。为了减少这种开销,操作系统引入了轻量级进程(LWP)。
-
轻量级进程(LWP):
- LWP是进程的一个变体,它在某些操作系统中被用来实现线程。LWP比传统的进程更轻量级,因为它们共享相同的地址空间和资源,这减少了上下文切换的开销。在某些系统中,如Solaris,LWP是实现线程的机制,而在其他系统,如Linux,线程是通过其他方式实现的。
-
线程共享地址空间:
- 在支持线程的操作系统中,同一进程中的多个线程共享相同的地址空间。这意味着线程可以直接访问彼此的数据,这提高了通信效率,但也引入了同步问题,因为线程需要协调对共享资源的访问以避免冲突。
-
Linux中的进程和线程:
- Linux操作系统在内核层面上不区分进程和线程。在Linux中,所有的并发执行单元都被视为进程,即使是用户空间的线程(也称为轻量级进程)。Linux内核通过
clone()
系统调用来创建新的进程或线程,这个调用允许指定新进程的许多属性,包括是否共享父进程的地址空间。 - 在用户空间,线程通常通过POSIX线程(pthreads)库来创建和管理。这些线程在内核中表现为轻量级进程,但用户空间的线程库会处理线程的创建、同步和调度,使得线程对用户程序来说更加透明和易于使用。
- Linux操作系统在内核层面上不区分进程和线程。在Linux中,所有的并发执行单元都被视为进程,即使是用户空间的线程(也称为轻量级进程)。Linux内核通过
在Linux中,线程的创建和管理通常比传统的进程更高效,因为线程之间共享资源,减少了上下文切换的开销。然而,线程的同步和死锁问题需要程序员特别注意。Linux内核的这种设计使得它在处理并发任务时既灵活又高效。
线程
POSIX线程(pthread)库是Unix和类Unix系统中用于创建和管理线程的一套API。它提供了一系列的函数来支持多线程编程,以下是一些基本的操作:
-
线程创建:
pthread_create()
:创建一个新的线程。
-
线程等待:
pthread_join()
:等待一个线程结束,并回收其资源。pthread_detach()
:使一个线程在后台运行,不等待其结束。
-
线程同步:
pthread_mutex_init()
:初始化一个互斥锁(mutex)。pthread_mutex_lock()
:锁定一个互斥锁。pthread_mutex_unlock()
:解锁一个互斥锁。pthread_mutex_destroy()
:销毁一个互斥锁。pthread_cond_init()
:初始化一个条件变量(condition variable)。pthread_cond_wait()
:等待条件变量。pthread_cond_signal()
:通知一个等待条件变量的线程。pthread_cond_broadcast()
:通知所有等待条件变量的线程。pthread_cond_destroy()
:销毁一个条件变量。
-
线程属性设置:
pthread_attr_init()
:初始化线程属性。pthread_attr_setdetachstate()
:设置线程的分离状态。pthread_attr_setstacksize()
:设置线程的堆栈大小。pthread_attr_destroy()
:销毁线程属性。
-
线程退出:
pthread_exit()
:线程正常退出。pthread_cancel()
:请求取消一个线程。
-
线程ID获取:
pthread_self()
:获取当前线程的ID。pthread_equal()
:比较两个线程ID是否相等。
-
线程局部存储(Thread-Local Storage, TLS):
pthread_key_create()
:创建一个新的线程局部存储键。pthread_key_delete()
:删除一个线程局部存储键。pthread_getspecific()
:获取与特定键关联的线程局部存储值。pthread_setspecific()
:设置与特定键关联的线程局部存储值。
-
线程优先级:
pthread_setschedparam()
:设置线程的调度参数。pthread_getschedparam()
:获取线程的调度参数。
-
线程状态获取:
pthread_kill()
:向一个线程发送信号。pthread_sigmask()
:设置或获取线程的信号掩码。
这些函数为多线程程序提供了必要的同步和并发控制机制,使得开发者能够编写出高效且安全的多线程应用程序。在使用这些函数时,需要注意线程安全和资源竞争问题,确保程序的正确性和性能。
pthread_create 创建线程
pthread_create()
是 POSIX 线程(pthread)库中的一个核心函数,用于在进程中创建一个新的线程。这个函数允许你并行执行任务,从而提高程序的执行效率。以下是 pthread_create()
函数的基本用法和一些关键点:
函数原型:
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
参数说明:
pthread_t *thread
:指向pthread_t
类型的指针,用于存储新创建线程的线程标识符(ID)。const pthread_attr_t *attr
:指向pthread_attr_t
类型的指针,用于指定新线程的属性。如果不需要设置特殊属性,可以传递NULL
。void *(*start_routine)(void *)
:线程启动时执行的函数,即线程的入口点。这个函数接受一个指向void
的指针作为参数,通常用于传递线程所需的数据。void *restrict arg
:传递给start_routine
函数的参数。
在C语言中,restrict 关键字用于告诉编译器某个指针是指向数组或对象的唯一的写入指针。这意味着编译器可以假设没有其他指针或引用指向这个数组或对象,从而允许编译器进行更优化的代码生成,例如消除一些不必要的数据副本。
返回值:
- 成功时返回 0。
- 失败时返回错误码,如
EAGAIN
(资源不足)或ENOMEM
(内存不足)。
使用示例:
#include <pthread.h>
#include <stdio.h>
// 线程函数,即线程的入口点
void *thread_function(void *arg) {
printf("Hello from thread %lu\n", (unsigned long)pthread_self());
return NULL;
}
int main() {
pthread_t thread_id;
// 创建线程
if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
perror("pthread_create");
return 1;
}
// 主线程继续执行...
printf("Main thread is running\n");
// 等待线程结束
pthread_join(thread_id, NULL);
return 0;
}
注意事项:
- 在创建线程之前,通常需要调用
pthread_init()
来初始化线程库。 - 创建线程时,如果传递了
pthread_attr_t
结构,可以设置线程的属性,如堆栈大小、调度策略等。 - 创建线程后,主线程和新创建的线程可以并行执行。主线程需要调用
pthread_join()
来等待新线程结束,或者使用pthread_detach()
使新线程在后台运行。 - 线程函数应该返回一个指向
void
的指针,通常返回NULL
。 - 在多线程程序中,需要注意线程安全问题,确保共享资源的正确访问和修改。
pthread_create()
是多线程编程的基础,正确使用它可以显著提高程序的并发性能。
pthread_exit 线程结束
https://man.archlinux.org/man/pthread_exit.3
https://man.archlinux.org/man/pthread_exit.3p
pthread_exit()
是 POSIX 线程(pthread)库中的一个函数,用于在线程中主动退出。这个函数允许线程在完成其任务后优雅地结束,而不是在函数返回时隐式地结束。这对于需要在线程结束时执行清理操作或者返回状态信息的场景非常有用。
函数原型:
void pthread_exit(void *retval);
参数说明:
retval
:这是一个指向void
类型的指针,可以指向任何类型的数据。这个参数指定了线程退出时的返回值。如果线程不需要返回任何数据,可以将这个参数设置为NULL
。需要注意的是,retval
指向的数据在线程退出后仍然需要有效,否则可能会导致未定义行为。
用法:
当你在线程函数中调用 pthread_exit()
时,线程会立即结束执行,并且可以返回一个值给创建它的线程(如果创建线程时使用了 pthread_join()
)。这个返回值可以通过 pthread_join()
函数的第二个参数获取。
示例:
#include <pthread.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
void* thread_func(void* arg) {
//线程执行的任务
printf("Thread is running...\n");
//线程完成任务后退出,返回一个值
pthread_exit((void*)10);
}
int main() {
pthread_t thread_id;
int thread_result;
//创建线程
if(pthread_create(&thread_id,NULL,thread_func,NULL) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
//等待线程结束,获取线程的返回值
if(pthread_join(thread_id,(void**)&thread_result) != 0) {
perror("pthread_join");
exit(EXIT_FAILURE);
}
// 输出线程的返回值
printf("Thread returned %d\n", (int)thread_result);
return 0;
}
注意事项:
pthread_exit()
只能用于线程函数,不能在主函数或其他普通函数中使用。- 当线程调用
pthread_exit()
时,它不会影响进程中的其他线程。只有调用pthread_join()
的线程会等待这个线程结束。 - 如果线程在执行过程中没有调用
pthread_exit()
,而是直接从线程函数返回,那么线程也会结束,但是不会传递任何返回值。 - 在线程结束时,应该确保所有共享资源的正确释放,以避免资源泄露。
pthread_exit()
是多线程编程中一个重要的函数,它提供了一种在线程完成任务后主动退出并返回结果的机制。正确使用这个函数可以提高程序的健壮性和可维护性。
pthread_join 等待线程
pthread_join()
是 POSIX 线程(pthread)库中的一个函数,用于等待一个线程结束并获取其返回值。这个函数在多线程编程中非常有用,因为它允许主线程或其他线程等待子线程完成执行,确保在子线程执行完毕后再进行后续操作。
函数原型:
int pthread_join(pthread_t thread, void **retval);
参数说明:
pthread_t thread
:指定要等待的线程的标识符(ID)。void **retval
:这是一个指向void*
类型的指针的指针。如果这个指针不为NULL
,那么当pthread_join()
成功时,它会存储目标线程的返回值。如果不需要获取返回值,可以将这个参数设置为NULL
。
返回值:
- 成功时返回 0。
- 失败时返回一个非零的错误码,例如:
EDEADLK
:目标线程已经退出,或者已经被其他线程通过pthread_join()
加入。EINVAL
:thread
参数无效,或者retval
参数指向的地址无效。ESRCH
:找不到指定的线程。
功能描述:
pthread_join()
函数会阻塞调用它的线程,直到指定的线程结束。如果线程正常结束,pthread_join()
会返回线程的退出状态(即线程通过 pthread_exit()
返回的值)。如果线程异常终止(例如,由于信号),pthread_join()
会返回一个非零值,并且 retval
参数将不会被设置。
使用示例:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
void* thread_func(void* arg) {
//线程执行的任务
printf("Thread is running...\n");
//线程完成任务后退出,返回一个值
pthread_exit((void*)10);
}
int main() {
pthread_t thread_id;
int thread_result;
//创建线程
if(pthread_create(&thread_id,NULL,thread_func,NULL) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
//等待线程结束,获取线程的返回值
if(pthread_join(thread_id,(void**)&thread_result) != 0) {
perror("pthread_join");
exit(EXIT_FAILURE);
}
// 输出线程的返回值
printf("Thread returned %d\n", (int)thread_result);
return 0;
}
在这个例子中,主线程创建了一个子线程,然后调用 pthread_join()
等待子线程结束。一旦子线程通过 pthread_exit()
返回值,主线程就可以通过 pthread_join()
获取这个值,并继续执行。
注意事项:
- 一个线程的返回值只能被一个
pthread_join()
调用获取。如果尝试多次获取同一个线程的返回值,后续的调用会失败。 - 如果线程在
pthread_join()
调用之前已经结束,pthread_join()
会立即返回,并且retval
参数会被设置为线程的退出状态。 - 如果线程被设置为分离状态(detached),则不能使用
pthread_join()
来等待它。分离状态的线程在结束时会自动释放资源,不需要其他线程来等待。
pthread_detach 线程分离函数
pthread_detach()
是 POSIX 线程(pthread)库中的一个函数,用于将一个线程设置为分离状态(detached)。分离状态的线程在结束时会自动释放其资源,而不需要其他线程调用 pthread_join()
来等待和回收资源。这在某些情况下非常有用,比如当你不需要关心线程的结束状态或者不需要从线程获取返回值时。
函数原型:
int pthread_detach(pthread_t thread);
参数说明:
pthread_t thread
:指定要设置为分离状态的线程的标识符。
返回值:
- 成功时返回 0。
- 失败时返回一个非零的错误码,例如:
EINVAL
:thread
参数无效,或者线程已经是分离状态。ESRCH
:找不到指定的线程。
功能描述:
pthread_detach()
函数将线程设置为分离状态。一旦线程处于分离状态,它就会在执行完毕后自动释放其资源,包括栈空间和线程描述符。这意味着你不需要(也不能)使用 pthread_join()
来等待这个线程结束。这对于执行后台任务的线程特别有用,因为主线程不需要等待这些线程完成就可以继续执行。
使用示例:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thrd_func(void *arg)
{
printf("i am detach.\n");
sleep(5);
printf("good\n");
pthread_exit((void *)77);
}
int main(void)
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thrd_func, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
exit(1);
}
ret = pthread_detach(tid);
if (ret != 0) {
fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));
exit(1);
}
sleep(1);
printf("main pid=%d, tid=&lu\n", getpid(), pthread_self());
pthread_exit((void *)0);
//必须调用pthread_exit()函数才能看到子线程的信息
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_function(void *arg) {
// 线程执行的任务
printf("Thread is running...\n");
sleep(5); // 模拟长时间任务
printf("Thread is exiting...\n");
return NULL;
}
int main() {
pthread_t thread_id;
pthread_attr_t thread_attr;
// 初始化线程属性
pthread_attr_init(&thread_attr);
// 设置线程为分离状态
int ret = pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
if (ret != 0) {
perror("pthread_attr_setdetachstate");
return 1;
}
// 创建线程,传入线程属性
ret = pthread_create(&thread_id, &thread_attr, thread_function, NULL);
if (ret != 0) {
perror("pthread_create");
return 1;
}
// 主线程继续执行其他任务
printf("Main thread continues...\n");
// 主线程不需要等待分离线程结束,因为它会自动回收资源
sleep(10);
// 销毁线程属性
pthread_attr_destroy(&thread_attr);
return 0;
}
在这个例子中,主线程创建了一个子线程,然后立即将其设置为分离状态。这样,即使主线程继续执行,子线程也会在完成其任务后自动释放资源。
注意事项:
- 一旦线程被设置为分离状态,就不能再使用
pthread_join()
来等待它。尝试这样做会导致EINVAL
错误。 - 分离状态的线程在结束时不会保留其退出状态,因此无法获取线程的返回值。
- 如果线程在调用
pthread_detach()
之前已经结束,那么这个调用可能会失败并返回EINVAL
错误。 - 在某些情况下,你可能需要在线程创建时就设置分离状态,这可以通过
pthread_attr_t
结构体和pthread_attr_setdetachstate()
函数来实现。
pthread_cancel函数
pthread_cancel 是 POSIX 线程(pthread)库中的一个函数,用于请求取消(终止)一个线程。这个函数并不立即终止线程,而是向目标线程发送一个取消请求。线程在接收到取消请求后,会在下一个取消点(Cancellation Point)处检查是否被取消,并根据当前的取消状态和类型来决定是否终止执行。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdlib.h>
void *thread_function(void *arg)
{
int i = 0;
while (true)
{
printf("Thread is running ,count: %d\n", i++);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thread_function, NULL);
if (ret != 0)
{
perror("pthread_create");
exit(EXIT_FAILURE);
}
sleep(5);
ret = pthread_cancel(tid);
if (ret != 0)
{
perror("pthread_cancel");
exit(EXIT_FAILURE);
}
printf("Main thread ends\n");
return 0;
}
线程的清理
pthread_cleanup_push()
和 pthread_cleanup_pop()
是 POSIX 线程(pthread)库中的两个函数,它们用于在线程退出时自动执行清理函数。这两个函数通常成对使用,以确保在线程正常退出或被取消时,能够执行一些必要的清理工作,如释放锁、关闭文件描述符等。
pthread_cleanup_push()
这个函数用于注册一个清理函数。当线程在执行到 pthread_cleanup_pop()
时,或者线程被取消(通过 pthread_cancel()
)时,或者线程调用 pthread_exit()
时,注册的清理函数将被执行。清理函数的执行顺序是后进先出(LIFO)。
函数原型:
void pthread_cleanup_push(void (*routine)(void *), void *arg);
routine
:指向清理函数的指针。arg
:传递给清理函数的参数。
pthread_cleanup_pop()
这个函数用于移除之前通过 pthread_cleanup_push()
注册的清理函数。如果 execute
参数为非零值(通常是 1),则在移除时执行该清理函数。
函数原型:
void pthread_cleanup_pop(int execute);
execute
:如果为非零值,表示在移除清理函数时执行它;如果为零值(0),则不执行。
使用示例
以下是一个简单的示例,展示了如何在线程中使用这两个函数:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
void cleanup_function(void *arg)
{
printf("Cleanup function called with argument: %s\n", (char *)arg);
}
void *thread_function(void *arg)
{
pthread_cleanup_push(cleanup_function, "First cleanup");
pthread_cleanup_push(cleanup_function, "Second cleanup");
int i = 0;
while (true)
{
printf("Thread is running ,count: %d\n", i++);
if (i == 15)
break;
sleep(1);
}
pthread_cleanup_pop(1); // 执行第二个清理函数
pthread_cleanup_pop(1); // 执行第一个清理函数
return NULL;
}
int main()
{
pthread_t thread;
int ret;
ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0)
{
perror("pthread_create");
exit(EXIT_FAILURE);
}
pthread_join(thread, NULL);
return 0;
}
在这个例子中,当线程执行到 pthread_cleanup_pop(1)
时,它会按照注册的顺序逆序执行清理函数。第一个 pthread_cleanup_pop(1)
会执行第二个注册的清理函数,第二个 pthread_cleanup_pop(1)
会执行第一个注册的清理函数。
请注意,pthread_cleanup_push()
和 pthread_cleanup_pop()
必须成对出现,且必须在同一个函数中。如果在 pthread_cleanup_push()
和 pthread_cleanup_pop()
之间有 return
语句,那么清理函数不会被执行,这可能会导致资源泄露或其他问题。
线程的同步与互斥
临界资源(Critical Resource):
临界资源是指在多线程或多进程环境中,需要被多个执行单元(如线程或进程)共享,但在同一时刻只能被一个执行单元访问的资源。这种资源的特点是,如果多个执行单元同时访问,可能会导致数据不一致、竞态条件(Race Condition)或其他并发问题。常见的临界资源包括共享内存、文件句柄、数据库连接、打印机等。
互斥锁(Mutex,全称 Mutual Exclusion Lock):
互斥锁是一种同步机制,用于保护临界资源,确保在同一时刻只有一个执行单元能够访问该资源。互斥锁通常通过一个特殊的数据结构实现,这个数据结构可以被设置为“锁定”或“解锁”状态。当一个执行单元需要访问临界资源时,它会尝试获取互斥锁。如果锁是解锁状态,执行单元会成功获取锁并进入临界区执行操作;如果锁是锁定状态,执行单元会被阻塞,直到锁被释放。互斥锁的目的是防止多个执行单元同时访问临界资源,从而避免并发冲突。
互斥机制(Mutual Exclusion Mechanism):
互斥机制是操作系统和并发编程中用于管理对临界资源访问的一系列策略和方法。这些机制确保了在任何给定时间点,只有一个执行单元能够执行临界区的代码。互斥机制不仅包括互斥锁,还可能包括其他同步原语,如信号量(Semaphore)、条件变量(Condition Variable)、读写锁(Read-Write Lock)等。这些机制共同工作,以确保并发执行的线程或进程能够安全、有效地共享资源,同时避免并发问题。
在实际应用中,互斥锁是实现互斥机制的一种常见方式,它通过简单的锁定和解锁操作来控制对临界资源的访问。正确使用互斥锁和其他同步机制对于编写高效、可靠的并发程序至关重要。
互斥锁的两种初始化方式
动态初始化互斥锁和静态初始化互斥锁的主要区别在于它们的初始化时机和方式。
-
静态初始化:
- 在编译时期完成初始化,即在程序启动时,互斥锁就已经被创建并初始化。
- 使用宏
PTHREAD_MUTEX_INITIALIZER
进行初始化,这通常适用于全局或静态变量。 - 静态初始化的互斥锁在程序的整个生命周期内都存在,不需要显式地销毁。
- 例如,在 C 语言中,你可以这样声明一个静态初始化的互斥锁:
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
-
动态初始化:
- 在运行时期进行初始化,即在程序执行过程中,根据需要创建互斥锁。
- 使用
pthread_mutex_init()
函数进行初始化,这适用于需要在运行时动态创建的互斥锁。 - 动态初始化的互斥锁需要在不再需要时显式地销毁,以释放资源,这通过
pthread_mutex_destroy()
函数完成。 - 例如,在 C 语言中,你可以这样动态初始化一个互斥锁:
pthread_mutex_t my_mutex; int result = pthread_mutex_init(&my_mutex, NULL); if (result != 0) { // 处理错误 } // 使用互斥锁... // ... // 当互斥锁不再需要时,销毁它 pthread_mutex_destroy(&my_mutex);
选择使用静态初始化还是动态初始化取决于你的具体需求。静态初始化更简单,因为它不需要额外的初始化代码,而且互斥锁在程序的整个生命周期内都可用。然而,如果你需要在程序的不同部分创建和销毁互斥锁,或者你不确定在程序启动时是否需要互斥锁,那么动态初始化可能更合适。动态初始化提供了更大的灵活性,但同时也需要更多的管理(如销毁互斥锁)。
没有互斥的情况
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
FILE *fp;
void *func1(void *arg)
{
pthread_detach(pthread_self());
printf("This is func1 child thread:tid=%ld\n", pthread_self());
char str[] = "you read func1 thread\n";
char c;
long unsigned int i = 0;
while (true)
{
while (i < strlen(str))
{
c = str[i++];
fputc(c, fp);
usleep(1000);
}
i = 0;
usleep(1);
}
pthread_exit((void *)"func1 thread return");
}
void *func2(void *arg)
{
pthread_detach(pthread_self());
printf("This is func2 child thread:tid=%ld\n", pthread_self());
char str[] = "I write func2 line\n";
char c;
long unsigned int i = 0;
while (true)
{
while (i < strlen(str))
{
c = str[i++];
fputc(c, fp);
usleep(1000);
}
i = 0;
usleep(1);
}
pthread_exit((void *)"func2 thread return");
}
int main()
{
pthread_t tid1, tid2;
fp = fopen("1.txt", "a+");
if (fp == NULL)
{
perror("fopen");
exit(EXIT_FAILURE);
}
pthread_create(&tid1, NULL, func1, NULL);
pthread_create(&tid2, NULL, func2, NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
上互斥锁
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
FILE *fp;
void *func1(void *arg)
{
pthread_detach(pthread_self());
printf("This is func1 child thread:tid=%ld\n", pthread_self());
char str[] = "you read func1 thread\n";
char c;
long unsigned int i = 0;
while (true)
{
pthread_mutex_lock(&mutex);
while (i < strlen(str))
{
c = str[i++];
fputc(c, fp);
usleep(1000);
}
pthread_mutex_unlock(&mutex);
i = 0;
usleep(1);
}
pthread_exit((void *)"func1 thread return");
}
void *func2(void *arg)
{
pthread_detach(pthread_self());
printf("This is func2 child thread:tid=%ld\n", pthread_self());
char str[] = "I write func2 line\n";
char c;
long unsigned int i = 0;
while (true)
{
pthread_mutex_lock(&mutex);
while (i < strlen(str))
{
c = str[i++];
fputc(c, fp);
usleep(1000);
}
pthread_mutex_unlock(&mutex);
i = 0;
usleep(1);
}
pthread_exit((void *)"func2 thread return");
}
int main()
{
pthread_t tid1, tid2;
fp = fopen("1.txt", "a+");
if (fp == NULL)
{
perror("fopen");
exit(EXIT_FAILURE);
}
pthread_create(&tid1, NULL, func1, NULL);
pthread_create(&tid2, NULL, func2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
}
读写锁
在 Linux 中,读写锁(Read-Write Lock)是一种用于多线程编程的同步机制,它允许多个线程同时读取共享资源,但只允许一个线程写入。这种锁的设计目的是为了提高并发性能,特别是在读操作远多于写操作的场景中。读写锁提供了更高的并行性,因为它允许多个读操作同时进行,从而减少了线程间的等待时间。
读写锁的主要特点如下:
-
读共享(Read Sharing):当一个线程以读模式(读锁)获取锁时,其他线程也可以以读模式获取同一锁。这意味着多个线程可以同时读取共享资源,而不会相互阻塞。
-
写独占(Write Exclusive):当一个线程以写模式(写锁)获取锁时,它会独占锁,此时其他线程(无论是读还是写)都无法获取锁,直到写锁被释放。
-
写优先(Writer Priority):在有多个线程等待读写锁时,写锁请求通常具有更高的优先级。这意味着如果有一个写锁请求和一个或多个读锁请求同时存在,写锁请求会优先被满足。
在 Linux 中,读写锁的实现依赖于 pthread_rwlock_t
类型,这是 POSIX 线程(pthread)库的一部分。以下是读写锁的一些主要操作函数:
pthread_rwlock_init()
:初始化读写锁。pthread_rwlock_rdlock()
:以读模式请求读写锁。pthread_rwlock_wrlock()
:以写模式请求读写锁。pthread_rwlock_tryrdlock()
:尝试以读模式请求读写锁,如果锁不可用则立即返回错误。pthread_rwlock_trywrlock()
:尝试以写模式请求读写锁,如果锁不可用则立即返回错误。pthread_rwlock_unlock()
:释放读写锁。pthread_rwlock_destroy()
:销毁读写锁。
使用读写锁时,开发者需要注意以下几点:
- 在释放读写锁之前,确保没有其他线程以读模式持有该锁。如果存在,需要等待这些线程释放锁。
- 在写操作完成后,确保及时释放写锁,以便其他线程可以进行读操作。
- 在设计程序时,应尽量减少写操作的频率,以充分利用读写锁的并行优势。
下面是一个简单的读写锁使用示例:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void* reader(void* arg) {
int id = *(int*)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock); // 以读模式加锁
printf("Reader %d is reading.\n", id);
pthread_rwlock_unlock(&rwlock); // 解锁
}
return NULL;
}
void* writer(void* arg) {
int id = *(int*)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock); // 以写模式加锁
printf("Writer %d is writing.\n", id);
pthread_rwlock_unlock(&rwlock); // 解锁
}
return NULL;
}
int main() {
pthread_t readers[5], writer;
int reader_ids[5] = {1, 2, 3, 4, 5};
int writer_id = 6;
// 创建读者线程
for (int i = 0; i < 5; ++i) {
pthread_create(&readers[i], NULL, reader, &reader_ids[i]);
}
// 创建写者线程
pthread_create(&writer, NULL, writer, &writer_id);
// 等待所有线程结束
for (int i = 0; i < 5; ++i) {
pthread_join(readers[i], NULL);
}
pthread_join(writer, NULL);
return 0;
}
在这个例子中,我们创建了五个读者线程和一个写者线程。读者线程以读模式获取读写锁并读取数据,而写者线程以写模式获取锁并写入数据。由于读写锁的特性,读者线程可以并行执行,而写者线程则独占锁进行写操作。
死锁
线程死锁(Thread Deadlock)是多线程编程中的一种现象,它发生在多个线程在执行过程中相互等待对方释放资源,导致所有相关线程都无法继续执行的情况。这种现象类似于“僵局”(stalemate),因为每个线程都在等待其他线程释放资源,而没有外部干预,它们将永远无法继续执行。
线程死锁通常涉及以下四个必要条件:
-
互斥条件(Mutual Exclusion):
至少有一个资源(如锁、文件句柄等)在同一时刻只能被一个线程使用。如果其他线程试图访问该资源,它必须等待直到资源被释放。 -
占有并等待(Hold and Wait):
一个线程已经持有至少一个资源,但还需要其他资源才能继续执行,而这些资源可能被其他线程持有。因此,它在等待其他线程释放资源的同时,仍然占有自己已经获取的资源。 -
不可剥夺(No Preemption):
线程持有的资源不能被其他线程强制剥夺,只能由持有资源的线程主动释放。 -
循环等待(Circular Wait):
存在一组线程,其中每个线程都在等待下一个线程释放资源。这种等待关系形成一个循环,每个线程都在等待前一个线程释放资源。
线程死锁可能导致系统性能下降,甚至导致系统完全停滞。为了避免或解决死锁,开发者需要采取相应的策略,如合理设计资源分配策略、使用锁的顺序、设置超时机制等。在某些情况下,系统可能需要具备死锁检测和恢复的能力,以便在死锁发生时能够采取适当的措施。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define THREAD_COUNT 2
pthread_mutex_t mutexes[THREAD_COUNT]; // 创建两个互斥锁
// 线程函数,模拟生产者和消费者
void *thread_func(void *arg)
{
int thread_id = *(int *)arg;
int i = 0;
// 模拟生产者和消费者的行为
while (i < 5)
{
if (thread_id == 0)
{
pthread_mutex_lock(&mutexes[0]);
pthread_mutex_lock(&mutexes[1]);
}
else
{
pthread_mutex_lock(&mutexes[1]);
pthread_mutex_lock(&mutexes[0]);
}
// 添加生产或者消费的逻辑
printf("Thread: %d: Locks acquired,performing work....\n");
sleep(1); // 模拟工作负载
// 释放互斥锁
pthread_mutex_unlock(&mutexes[0]);
pthread_mutex_unlock(&mutexes[1]);
i++;
}
return NULL;
}
int main()
{
pthread_t threads[THREAD_COUNT];
int thread_ids[THREAD_COUNT] = {0, 1};
// 初始化互斥锁
for (int i = 0; i < THREAD_COUNT; ++i)
{
pthread_mutex_init(&mutexes[i], NULL);
}
// 创建线程
for (int i = 0; i < THREAD_COUNT; i++)
{
pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]);
}
// 等待线程结束
for (int i = 0; i < THREAD_COUNT; i++)
{
pthread_join(threads[i], NULL);
}
// 销毁互斥锁
for (int i = 0; i < THREAD_COUNT; i++)
{
pthread_mutex_destroy(&mutexes[i]);
}
return 0;
}
生产者,消费者模型
生产者-消费者模型(Producer-Consumer Model)是并发编程中的一个经典问题,它描述了生产者和消费者之间的交互关系。在这个模型中,生产者负责生成数据,而消费者则负责消费这些数据。两者通过一个共享的缓冲区(通常是一个队列)进行数据交换。为了确保数据的正确传递和系统的稳定性,通常需要使用同步机制来控制生产者和消费者对缓冲区的访问。
在C语言中,实现生产者-消费者模型通常涉及以下步骤:
-
定义共享缓冲区:创建一个固定大小的数组或链表作为缓冲区,用于存储生产者生成的数据。
-
使用互斥锁(Mutex):为了防止生产者和消费者同时访问缓冲区,需要使用互斥锁来保护缓冲区的访问。
-
使用条件变量(Condition Variable):条件变量用于在生产者和消费者之间传递信号。当缓冲区为空时,消费者等待;当缓冲区满时,生产者等待。
-
生产者线程:生产者线程在生成数据后,会检查缓冲区是否已满。如果已满,它会等待直到有空间。然后,生产者将数据放入缓冲区,并通知消费者。
-
消费者线程:消费者线程在开始消费之前,会检查缓冲区是否为空。如果为空,它会等待直到有数据。然后,消费者从缓冲区取出数据,并通知生产者可以继续生产。
-
同步操作:在生产者和消费者线程中,使用
pthread_cond_wait()
和pthread_cond_signal()
或pthread_cond_broadcast()
来实现同步。
以下是一个简化的生产者-消费者模型的C语言实现示例:
#include <stdio.h>
#include <pthread.h>
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&full, &mutex);
}
// 生产数据并放入缓冲区
buffer[in] = ...;
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
// 模拟生产时间
}
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&empty, &mutex);
}
// 从缓冲区取出数据并消费
int data = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count--;
pthread_cond_signal(&full);
pthread_mutex_unlock(&mutex);
// 模拟消费时间
}
}
int main() {
pthread_t producer, consumer;
pthread_create(&producer, NULL, producer, NULL);
pthread_create(&consumer, NULL, consumer, NULL);
pthread_join(producer, NULL);
pthread_join(consumer, NULL);
return 0;
}
在这个示例中,生产者和消费者线程通过互斥锁和条件变量来同步对缓冲区的访问。生产者在缓冲区满时等待,消费者在缓冲区空时等待。当条件满足时,相应的线程会被唤醒并继续执行。这种模式有效地分离了生产和消费的过程,提高了系统的并发性能。
条件变量
这个代码片段是一个经典的生产者-消费者模型,使用了条件变量来实现线程间的同步。在这个模型中,生产者线程负责生产数据并将其放入队列,而消费者线程则从队列中取出数据进行消费。条件变量 not_full
和 not_empty
分别用于指示队列是否已满和是否为空,以便生产者和消费者线程可以等待合适的时机来执行操作。
让我们逐行解释关键部分:
生产者线程 (producer
函数)
void *producer(void *arg)
{
Buffer *b = (Buffer *)arg;
int item;
while (1)
{
// 生产数据
item = rand() % 100;
// 加锁,确保生产者线程在操作队列时不会与其他线程冲突
pthread_mutex_lock(&b->lock);
// 等待队列非满
while (b->count == BUFFER_SIZE)
{
pthread_cond_wait(&b->not_full, &b->lock);
}
// 当队列不满时,生产者线程会从这里继续执行
// 放入数据到队列
b->items[b->in] = item;
b->in = (b->in + 1) % BUFFER_SIZE; // 更新生产者索引
b->count++; // 增加队列中的元素计数
// 唤醒一个等待的消费者线程,如果有的话
pthread_cond_signal(&b->not_empty);
// 解锁,允许其他线程访问队列
pthread_mutex_unlock(&b->lock);
printf("Producer produced %d\n", item);
sleep(1); // 模拟生产时间
}
}
消费者线程 (consumer
函数)
void *consumer(void *arg)
{
Buffer *b = (Buffer *)arg;
int item;
while (true)
{
// 加锁,确保消费者线程在操作队列时不会与其他线程冲突
pthread_mutex_lock(&b->lock);
// 等待队列非空
while (b->count == 0)
{
pthread_cond_wait(&b->not_empty, &b->lock);
}
// 当队列不空时,消费者线程会从这里继续执行
// 从队列中取出数据
item = b->items[b->out];
b->out = (b->out + 1) % BUFFER_SIZE; // 更新消费者索引
b->count--; // 减少队列中的元素计数
// 唤醒一个等待的生产者线程,如果有的话
pthread_cond_signal(&b->not_full);
// 解锁,允许其他线程访问队列
pthread_mutex_unlock(&b->lock);
printf("Consumer consumed %d\n", item);
sleep(1); // 模拟消费时间
}
return NULL;
}
关键点解释
-
条件变量 (
pthread_cond_t
):pthread_cond_wait()
:当条件不满足时(例如,队列满或空),线程会释放互斥锁并进入等待状态。直到另一个线程通过pthread_cond_signal()
或pthread_cond_broadcast()
唤醒它。pthread_cond_signal()
:唤醒等待在条件变量上的一个线程。pthread_cond_broadcast()
:唤醒所有等待在条件变量上的线程。
-
互斥锁 (
pthread_mutex_t
):- 在生产者和消费者线程中,互斥锁用于保护对共享资源(队列)的访问。在操作队列之前,线程必须先获取互斥锁,操作完成后再释放。
-
等待逻辑:
- 生产者线程在队列满时等待(
b->count == BUFFER_SIZE
),直到有空间(通过pthread_cond_wait()
)。 - 消费者线程在队列为空时等待(
b->count == 0
),直到有数据(通过pthread_cond_wait()
)。
- 生产者线程在队列满时等待(
-
唤醒逻辑:
- 生产者线程在放入数据后唤醒消费者(
pthread_cond_signal(&b->not_empty)
),表示队列中有数据了。 - 消费者线程在取出数据后唤醒生产者(
pthread_cond_signal(&b->not_full)
),表示队列有空间了。
- 生产者线程在放入数据后唤醒消费者(
-
索引更新:
b->in
和b->out
分别表示生产者和消费者的索引。每次操作后,索引都会递增,并在达到队列大小后回绕(% BUFFER_SIZE
)。
-
计数器
b->count
:- 用于跟踪队列中的元素数量。生产者增加计数器,消费者减少计数器。
这个模型通过这些同步机制确保了生产者不会在队列满时生产数据,消费者也不会在队列为空时消费数据,从而避免了潜在的竞态条件。同时,它允许生产者和消费者以异步的方式工作,提高了程序的并发性能。
在这个生产者-消费者模型中,生产者和消费者通过条件变量和互斥锁来协调它们的工作流程。下面是详细的工作流程:
-
生产者(Producer):
- 生产者线程生成数据(例如,通过
rand() % 100
生成一个随机数)。 - 生产者获取互斥锁,以确保在向队列中添加数据时不会与其他线程发生冲突。
- 如果队列已满(
b->count == BUFFER_SIZE
),生产者会调用pthread_cond_wait()
进入等待状态,直到队列有空间。在等待期间,生产者释放互斥锁,允许其他线程(如消费者)访问队列。 - 当队列有空间时,生产者将数据放入队列,并更新生产者索引
b->in
和队列计数b->count
。 - 生产者唤醒一个等待的消费者线程(如果有的话),通过调用
pthread_cond_signal(&b->not_empty)
。 - 生产者释放互斥锁,允许其他线程访问队列。
- 生产者线程生成数据(例如,通过
-
消费者(Consumer):
- 消费者线程准备从队列中消费数据。
- 消费者获取互斥锁,以确保在从队列中取出数据时不会与其他线程发生冲突。
- 如果队列为空(
b->count == 0
),消费者会调用pthread_cond_wait()
进入等待状态,直到队列中有数据。在等待期间,消费者释放互斥锁,允许其他线程(如生产者)访问队列。 - 当队列非空时,消费者从队列中取出数据,并更新消费者索引
b->out
和队列计数b->count
。 - 消费者唤醒一个等待的生产者线程(如果有的话),通过调用
pthread_cond_signal(&b->not_full)
。 - 消费者释放互斥锁,允许其他线程访问队列。
这个模型的关键点在于,生产者和消费者都通过条件变量来等待对方完成操作,而不是通过忙等待(busy-waiting),这大大提高了效率。同时,互斥锁确保了在任何时刻只有一个线程可以修改队列,从而避免了数据竞争和不一致的问题。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <stdbool.h>
#define BUFFER_SIZE 20
#define MAX_ITEMS 10 // 设置循环次数
typedef struct
{
int items[BUFFER_SIZE];
int in; // 生产者放入位置
int out; // 消费者取出位置
int count; // 当前队列中的元素数量
pthread_mutex_t lock; // 互斥锁
pthread_cond_t not_full; // 非满条件变量
pthread_cond_t not_empty; // 非空条件变量
} Buffer;
// 初始化队列
void init_buffer(Buffer *b)
{
b->in = 0;
b->out = 0;
b->count = 0;
pthread_mutex_init(&b->lock, NULL);
pthread_cond_init(&b->not_full, NULL);
pthread_cond_init(&b->not_empty, NULL);
}
// 生产者线程
void *producer(void *arg)
{
Buffer *b = (Buffer *)arg;
int item;
while (1)
{
// 生产数据
item = rand() % 100;
// 加锁
pthread_mutex_lock(&b->lock);
// 等待队列非满
while (b->count == BUFFER_SIZE)
{
pthread_cond_wait(&b->not_full, &b->lock);
}
// 放入数据
b->items[b->in] = item;
b->in = (b->in + 1) % BUFFER_SIZE;
b->count++;
// 唤醒消费者
pthread_cond_signal(&b->not_empty);
// 解锁
pthread_mutex_unlock(&b->lock);
printf("Producer produced %d\n", item);
sleep(1); // 模拟生产时间
}
}
// 消费者线程
void *consumer(void *arg)
{
Buffer *b = (Buffer *)arg;
int item;
while (true)
{
// 加锁
pthread_mutex_lock(&b->lock);
// 等待队列非空
while (b->count == 0)
{
pthread_cond_wait(&b->not_empty, &b->lock);
}
// 取除数据
item = b->items[b->out];
b->out = (b->out + 1) % BUFFER_SIZE;
b->count--;
// 唤醒生产者
pthread_cond_signal(&b->not_full);
// 解锁
pthread_mutex_unlock(&b->lock);
printf("Consumer consumed %d\n", item);
sleep(1); // 模拟消费时间
}
return NULL;
}
int main()
{
Buffer buffer;
pthread_t producer_thread, consumer_thread;
int producer_count = 0, consumer_count = 0; // 循环计数器
// 初始化队列
init_buffer(&buffer);
// 创建生产者线程
pthread_create(&producer_thread, NULL, producer, &buffer);
// 创建消费者线程
pthread_create(&consumer_thread, NULL, consumer, &buffer);
// 等待生产者和消费者线程结束
while (producer_count < MAX_ITEMS && consumer_count < MAX_ITEMS)
{
producer_count++;
consumer_count++;
// 这里可以添加其他逻辑,例如打印进度信息
sleep(1); // 模拟等待时间,避免频繁检查
}
// 通知线程结束
pthread_cancel(producer_thread);
pthread_cancel(consumer_thread);
// 等待线程真正结束
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
// 销毁条件变量和互斥锁
pthread_cond_destroy(&buffer.not_full);
pthread_cond_destroy(&buffer.not_empty);
pthread_mutex_destroy(&buffer.lock);
return 0;
}
线程池
https://zhuanlan.zhihu.com/p/141615042
https://github.com/Zhuweizhoong/C_Threadpool/blob/master/threadpool.c
上面大佬写的很好