目录
一、多线程的基本编程
二、线程安全(同步互斥)
1.使用互斥锁达到互斥
2.使用互斥锁和条件变量达到线程间的同步互斥
一、多线程的基本编程
线程回收:线程在运行时需要分配内存空间、处理器时间等系统资源,这些资源在线程结束后应当被释放,以便其他线程或进程能够继续使用它们。如果不回收线程,就会导致系统资源的浪费和资源泄漏问题。
1.join回收(常用)
使用 pthread_join
函数可以实现线程的等待和回收效果。具体来说,调用 pthread_join
函数会使当前线程(通常是主线程)等待指定的线程结束执行,然后将其资源回收。
当调用 pthread_join
后,主线程会阻塞直到指定的线程结束。这意味着主线程会等待被等待线程执行完成后再继续执行后续的代码。如果不使用 pthread_join
,主线程可能会在被等待线程执行完之前就结束,这样被等待线程的资源可能无法得到回收造成僵尸线程,可能导致资源泄漏。
因此,使用 pthread_join
函数可以确保线程的资源在合适的时候被回收,保证程序的正确性和稳定性。
pthread_create
: 这个函数用于创建一个新线程。它接受四个参数:第一个参数是指向线程标识符的指针,用于存储新线程的标识符;第二个参数是用于设置线程属性的指针,通常为 NULL,表示使用默认属性;第三个参数是一个函数指针,指向我们想要在线程中执行的函数;第四个参数是传递给线程函数的参数。
pthread_join
: 这个函数用于等待指定的线程结束执行。它接受两个参数:第一个参数是要等待的线程的标识符;第二个参数是一个指向线程返回值的指针。在这个示例中,我们传递了 NULL,表示我们不关心线程的返回值。
pthread_exit
: 这个函数用于终止调用它的线程。在线程函数中,当线程完成任务后,应该使用pthread_exit
来终止线程。通常情况下,线程在结束时会调用
pthread_exit
来主动结束自己的执行,并且可以通过传递一个退出状态来向其他线程传递信息。然后,其他线程可以使用pthread_join
来等待该线程的结束,并获取它的退出状态。所以当一个线程调用pthread_join
来等待另一个线程结束时,被等待的线程可以通过pthread_exit
函数传递一个退出状态,这个退出状态会被pthread_join
函数接收到,并存储在指定的变量中。这样,主线程或者其他等待线程就可以获取到被等待线程的返回值。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 一个简单的线程函数,打印一些文本
void *print_message(void *ptr)
{
char *message = (char *)ptr;
while(1)
{
printf("%s\n", message);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t thread1, thread2;
char *message1 = "Thread 1: Hello, world!";
char *message2 = "Thread 2: Goodbye, world!";
// 创建线程1
if (pthread_create(&thread1, NULL, print_message, (void *)message1) != 0)
{
fprintf(stderr, "Error creating thread 1\n");
return 1;
}
// 创建线程2
if (pthread_create(&thread2, NULL, print_message, (void *)message2) != 0)
{
fprintf(stderr, "Error creating thread 2\n");
return 1;
}
// 等待线程1结束
if (pthread_join(thread1, NULL) != 0)
{
fprintf(stderr, "Error joining thread 1\n");
return 1;
}
// 等待线程2结束
if (pthread_join(thread2, NULL) != 0)
{
fprintf(stderr, "Error joining thread 2\n");
return 1;
}
return 0;
}
2. 也可以通过调用 pthread_detach
函数将线程标记为“分离”的。这样,线程在结束时会自动释放资源,而不需要显式地调用 pthread_join
来等待和回收。分离线程一般用于不需要等待其完成的情况,比如后台任务。
调用 pthread_detach
后,即使主线程结束,被分离的线程仍然会继续执行,直到它结束或者被取消。分离状态的线程无法被其他线程等待和回收,因此它们在后台执行,并且不会产生僵尸线程。
需要注意的是,一旦线程被设置为分离状态,就不能再将其恢复为非分离状态。因此,在调用 pthread_detach
之前,需要确保这个线程不再需要被其他线程等待和回收。
但是如果主线程退出了,分离线程也会被终止。整个进程的生命周期是由主线程控制的。当主线程退出时,整个进程会被终止,这也包括所有的分离线程。
分离线程的主要特点是,它们不会阻止进程的终止,即使它们仍在执行。当主线程结束时,系统会自动回收分离线程的资源,并终止它们的执行。因此,分离线程通常用于执行一些不需要主线程等待的后台任务,但在主线程退出时,它们也会被终止。所以使用 pthread_detach
分离线程的一个潜在问题是,如果主线程在分离线程任务开始执行之前就结束了,那么分离线程可能没有机会完成它的任务。这种情况下,分离线程会被立即终止,而不会执行任何任务。
因此,在使用 pthread_detach
分离线程时,需要确保主线程不会过早退出,以允许分离线程有足够的时间来执行其任务。否则,可以考虑其他方式来管理线程,例如使用非分离线程或者适当地设计主线程的生命周期。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 一个简单的线程函数,打印一些文本
void *print_message(void *ptr)
{
char *message = (char *)ptr;
printf("%s\n", message);
return NULL;
}
int main() {
pthread_t thread1, thread2;
char *message1 = "Thread 1: Hello, world!";
char *message2 = "Thread 2: Goodbye, world!";
// 创建线程1并设置为分离状态
if (pthread_create(&thread1, NULL, print_message, (void *)message1) != 0)
{
fprintf(stderr, "Error creating thread 1\n");
return 1;
}
pthread_detach(thread1);
// 创建线程2并设置为分离状态
if (pthread_create(&thread2, NULL, print_message, (void *)message2) != 0)
{
fprintf(stderr, "Error creating thread 2\n");
return 1;
}
pthread_detach(thread2);
// 主线程可以继续执行其他操作,不需要等待分离线程结束
// 睡眠一段时间以确保分离线程有足够的时间打印信息
sleep(1);
return 0;
}
3.pthread_attr_destroy(不常用,看具体场景使用)
使用pthread_attr_destroy
函数来销毁线程属性对象 attr
。这个函数的作用是销毁先前通过 pthread_attr_init
初始化的线程属性对象,释放其所占用的资源。
在示例中,我们在创建线程前初始化了一个线程属性对象 attr
,然后通过 pthread_attr_setdetachstate
设置了线程的分离状态。在设置完线程属性后,我们调用了 pthread_attr_destroy(&attr)
来销毁这个线程属性对象,因为在后续的代码中不再需要它了。
总的来说,pthread_attr_destroy
函数用于释放不再需要的线程属性对象,以避免资源泄漏。
pthread_attr_destroy
函数通常在需要动态创建和销毁线程属性对象时使用。它的使用频率相对较低,因为大多数情况下,线程属性对象是在函数内部或者全局定义的静态对象,不需要销毁。
int pthread_attr_destroy(pthread_attr_t *attr);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 一个简单的线程函数,打印一些文本
void *print_message(void *ptr)
{
char *message = (char *)ptr;
printf("%s\n", message);
return NULL;
}
int main() {
pthread_t thread1, thread2;
char *message1 = "Thread 1: Hello, world!";
char *message2 = "Thread 2: Goodbye, world!";
// 创建线程1并设置为分离状态
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&thread1, &attr, print_message, (void *)message1) != 0)
{
fprintf(stderr, "Error creating thread 1\n");
return 1;
}
// 创建线程2并设置为分离状态
if (pthread_create(&thread2, &attr, print_message, (void *)message2) != 0)
{
fprintf(stderr, "Error creating thread 2\n");
return 1;
}
// 不再需要 attr,可以销毁
pthread_attr_destroy(&attr);
// 主线程可以继续执行其他操作,不需要等待分离线程结束
// 睡眠一段时间以确保分离线程有足够的时间打印信息
sleep(1);
return 0;
}
二、线程安全(同步互斥)
1.使用互斥锁达到互斥
互斥锁(Mutex)是一种用于多线程编程的同步机制,用于保护共享资源免受并发访问的影响。互斥锁可以确保在任何时刻,只有一个线程可以访问被保护的资源,从而避免了竞争条件(Race Condition)的发生。
想象一下你和朋友在玩一个游戏,这个游戏有一把钥匙,你们两个需要轮流使用这把钥匙。问题是,如果你们两个同时试图拿起钥匙,可能会发生什么?或者当一个人正在使用钥匙的时候,另一个人也想拿走钥匙会发生什么?
这个时候,互斥锁就像是一把只有一个人能够拿起的钥匙。当你需要使用这个共享资源(比如说一个全局变量、一个文件等)时,你会先尝试去获取这把互斥锁(也就是拿起钥匙)。如果没有其他人正在使用它,你就可以顺利地获取到互斥锁(拿到钥匙),然后就可以安全地使用这个共享资源了。一旦你使用完毕,你会把互斥锁(钥匙)放回原处,让其他人可以继续使用。
但是,如果另一个人已经拿着这把互斥锁(钥匙),那么你就需要等待,直到他释放出互斥锁。这就保证了在同一时刻,只有一个线程可以访问共享资源,避免了数据的混乱和不一致性。
使用流程:
初始化互斥锁
在使用互斥锁之前,需要先初始化互斥锁。可以使用 pthread_mutex_init
函数进行初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁(Lock)
在访问共享资源之前,线程需要先尝试获取互斥锁的所有权,这称为加锁。只有成功获取锁的线程才能访问共享资源。可以使用 pthread_mutex_lock
函数来加锁
pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);
解锁(Unlock)
在线程完成对共享资源的访问后,需要释放互斥锁,以允许其他线程访问共享资源。可以使用 pthread_mutex_unlock
函数来解锁:
pthread_mutex_unlock(&mutex);
销毁互斥锁
在不再需要互斥锁时,需要将其销毁以释放相关资源。可以使用 pthread_mutex_destroy
函数来销毁互斥锁:
pthread_mutex_destroy(&mutex);
注意事项
- 加锁和解锁必须成对出现,否则可能导致死锁或其他问题。
- 加锁的线程必须记得在不再需要共享资源时释放锁,否则可能导致其他线程无法访问共享资源。
- 尽量避免在加锁期间执行耗时操作,以减少锁的持有时间,提高并发性能。
通过合理地使用互斥锁,可以确保多线程程序中的共享资源得到正确地访问和修改,避免了竞争条件和数据不一致的问题。
补充:尝试加锁
如果一个线程尝试获取锁但失败了(因为锁已经被其他线程获取了),它可以选择阻塞等待,也可以尝试获取锁并立即返回结果。后者称为尝试加锁,可以使用 pthread_mutex_trylock
函数来尝试加锁。
pthread_mutex_trylock
函数提供了一种非阻塞的方式来尝试获取互斥锁。当线程调用 pthread_mutex_trylock
尝试获取锁时,如果互斥锁当前已经被其他线程占用,那么该函数会立即返回,并返回一个非零值,表示获取锁失败。线程可以根据这个返回值来判断是否获取了锁。
这种方式的优势在于,如果线程在尝试获取锁时不希望被阻塞,可以使用 pthread_mutex_trylock
函数,它会立即返回,线程可以继续执行其他操作。同时,通过不断地尝试获取锁,线程可以在后续的尝试中成功地获取到锁,而不需要一直等待锁的释放。
与之相对应的是上面pthread_mutex_lock
函数,它会在锁被其他线程占用时阻塞当前线程,直到获取到锁为止。pthread_mutex_lock
适用于那些必须立即获取锁才能继续执行的情况。
if (pthread_mutex_trylock(&mutex) == 0)
{ // 成功获取锁,访问共享资源
pthread_mutex_unlock(&mutex);
}
else
{ // 锁被其他线程占用,执行其他操作
}
(1)基于join回收的互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
pthread_mutex_t mutex;
int counter = 0;
void *thread_function(void *arg)
{
pthread_mutex_lock(&mutex);
printf("Thread %ld: Counter = %d\n", (long)arg, ++counter);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main()
{
pthread_t threads[NUM_THREADS];
int i;
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < NUM_THREADS; i++)
{
if (pthread_create(&threads[i], NULL, thread_function, (void *)i) != 0)
{
fprintf(stderr, "Error creating thread %d\n", i);
return 1;
}
}
for (i = 0; i < NUM_THREADS; i++)
{
if (pthread_join(threads[i], NULL) != 0)
{
fprintf(stderr, "Error joining thread %d\n", i);
return 1;
}
}
pthread_mutex_destroy(&mutex);
return 0;
}
(2)基于使用 pthread_detach
分离回收的互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
pthread_mutex_t mutex;
int counter = 0;
void *thread_function(void *arg)
{
pthread_mutex_lock(&mutex);
printf("Thread %ld: Counter = %d\n", (long)arg, ++counter);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main() {
pthread_t threads[NUM_THREADS];
int i;
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < NUM_THREADS; i++)
{
if (pthread_create(&threads[i], NULL, thread_function, (void *)i) != 0)
{
fprintf(stderr, "Error creating thread %d\n", i);
return 1;
}
}
for (i = 0; i < NUM_THREADS; i++)
{
pthread_detach(threads[i]);
}
// 主线程等待一段时间,以确保分离线程有足够的时间来执行
sleep(1);
pthread_mutex_destroy(&mutex);
return 0;
}
2.使用互斥锁和条件变量达到线程间的同步互斥
条件变量是一种用于线程间通信的同步机制,主要用于在线程等待某个条件满足时进行阻塞,以避免忙等待,从而提高系统效率。条件变量通常与互斥锁配合使用,用于保护共享资源的访问,并在共享资源状态发生变化时通知等待的线程。
-
避免忙等待: 在某些情况下,线程需要等待某个条件变为真,但不希望采用忙等待的方式不断地检查条件是否满足,以免占用过多的 CPU 资源。条件变量可以让线程在条件不满足时进入阻塞状态,直到条件被满足时才唤醒线程。
-
提高系统效率: 使用条件变量可以降低系统资源的消耗,因为线程在等待条件变量时会进入休眠状态,不再占用 CPU 资源,从而提高了系统的整体效率。
下面是一个基于互斥锁和条件变量的多线程示例,其中线程 A 负责生产数据,线程 B 负责消费数据:
在这个示例中,两个线程共享一个缓冲区 buffer
,producer
线程负责向缓冲区中生产数据,consumer
线程负责消费数据。通过互斥锁 mutex
和条件变量 cond
来保护缓冲区的访问,当缓冲区为空时,consumer
线程会进入等待状态,直到有数据被生产出来;当缓冲区已满时,producer
线程会进入等待状态,直到有数据被消费掉。
这就是典型的生产者-消费者模型,其中消费者在缓冲区为空时会阻塞等待生产者的通知,而生产者在缓冲区已满时会阻塞等待消费者的通知。这种通过条件变量和互斥锁实现的同步机制,能够确保生产者和消费者之间的协调与同步,有效地避免了竞态条件和数据不一致的问题。
其实只也可以生产者一直生产,生产好了通知消费者,消费者不用通知生产者。消费者可以不需要通知生产者,因为生产者一直在生产数据。在这种情况下,消费者只需要等待生产者通知,当生产者生产好数据时,通知消费者进行消费即可。这种模型更符合实际生产消费的场景,因为生产者负责生产,而消费者负责消费,双方各司其职,相互协作。下面这个例子是使用相互通知的,具体场景需要自己灵活选择;
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define BUFFER_SIZE 10
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer[BUFFER_SIZE];
int count = 0;
void *producer(void *arg)
{
int data = 0;
while (1)
{
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE)
{
pthread_cond_wait(&cond, &mutex);
}
buffer[count++] = data++;
printf("Produced: %d\n", data);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(500000); // 模拟生产数据的耗时操作
}
pthread_exit(NULL);
}
void *consumer(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex);
while (count == 0)
{
pthread_cond_wait(&cond, &mutex);
}
int data = buffer[--count];
printf("Consumed: %d\n", data);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(800000); // 模拟消费数据的耗时操作
}
pthread_exit(NULL);
}
int main()
{
pthread_t producer_thread, consumer_thread;
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
return 0;
}