本文主要涉及多线程的使用方法,通过两个实例来对多线程的使用进行理解,
案例包括:
1.一个线程负责计数,另一个线程负责打印计数值
2.消费者生产者问题
线程:线程是进程的一部分,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程
,这些线程共享进程的资源(如内存),但每个线程都有自己的堆栈和局部变量。
多线程:多线程是指一个进程中有多个执行路径,每个执行路径都称为一个线程。多线程允许程序同时执行多个任务,增强了程序的并发性和响应性。
考虑一个简单的场景:有两个线程,一个线程负责计数,另一个线程负责打印计数值。
#include
#include
#include // 用于 usleep 函数
int count = 0;
pthread_mutex_t count_mutex;
void *increment_counter(void *arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&count_mutex);
count++;
pthread_mutex_unlock(&count_mutex);
usleep(300000); // 休眠 300ms
}
pthread_exit(NULL);
}
void *print_counter(void *arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&count_mutex);
printf("Count: %d\n", count);
pthread_mutex_unlock(&count_mutex);
usleep(300000); // 休眠 300ms
}
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&count_mutex, NULL);
pthread_create(&thread1, NULL, increment_counter, NULL);
pthread_create(&thread2, NULL, print_counter, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&count_mutex);
return 0;
}
在上述例子中,两个线程并发地增加和打印计数值,为了确保计数的正确性,使用了互斥锁。其中有两个线程:increment_counter
和 print_counter
。其中,一个线程用于增加一个全局计数器 count
的值,而另一个线程则用于打印这个计数器的值。
以下是代码的工作流程:
全局变量和互斥锁:定义了一个全局变量 count
和一个互斥锁 count_mutex
。互斥锁用于确保同时只有一个线程可以修改 count
的值,从而避免了竞争条件。
增加计数器的线程:increment_counter
函数负责增加 count
的值。在每次增加之前,线程会锁定互斥锁,然后在增加后解锁它。此外,为了模拟一些处理时间,线程使用 usleep
函数休眠 300 毫秒。
打印计数器的线程:print_counter
函数负责打印 count
的当前值。它也使用相同的互斥锁来确保在打印之前和之后正确地锁定和解锁。
主函数:在 main
函数中,首先初始化互斥锁。然后创建两个线程,一个用于增加计数器,另一个用于打印计数器的值。使用 pthread_join
确保主线程等待这两个线程完成后再继续执行。最后,清理互斥锁并退出程序。
运行此程序时,您应该会看到交替的输出,其中每个值都是先增加线程更改后的最新值。由于使用了互斥锁,所以不会出现不一致或竞争条件的情况。
如果没有这个延迟,可能会看到一些奇怪的行为,甚至可能出现全是10的情况。这是因为线程调度的机制。
在多线程环境中,操作系统会为每个线程分配时间片(也称为CPU时间),一个线程在分配的时间片用完后,操作系统可能会将其挂起并切换到另一个线程。这种切换是不确定的,即无法预测两个线程之间的交替顺序。
如果没有延迟,increment_counter
线程可能会迅速地递增 count
到10,然后print_counter
线程开始运行,因为没有其他操作或延迟来改变这种行为。因此,可能会看到很多行都是 Count: 10
。
但是,当在关键部分(如对 count
的递增和打印)添加了延迟时,会更有机会看到这两个线程之间的交互,因为操作系统的线程调度会更频繁地进行线程切换,从而使得递增和打印的操作交错进行。
延迟在这里有助于模拟和增强多线程的交互效果,使得并发问题更容易观察和理解。
条件变量 (Condition Variables) 是一种线程同步机制,它允许一个或多个线程等待某个特定的条件得到满足后再继续执行。它通常与互斥量 (Mutex) 一起使用,以确保在检查条件和等待条件之间的操作是原子的。
初始化条件变量:
pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER;
等待条件满足:
pthread_cond_wait(&cond_var, &mutex);
此函数会释放互斥量并阻塞当前线程,直到另一个线程调用 pthread_cond_signal
或 pthread_cond_broadcast
并重新获得互斥量。
发送信号:
pthread_cond_signal(&cond_var);
此函数唤醒一个等待在条件变量上的线程。如果有多个线程等待,那么哪一个会被唤醒是不确定的。
广播信号:
pthread_cond_broadcast(&cond_var);
此函数唤醒所有等待在条件变量上的线程。
考虑一个经典的生产者-消费者问题,其中生产者线程在一个有限的缓冲区中放入数据,而消费者线程从缓冲区中取出数据。
#include
#include
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) {
pthread_cond_wait(&cond_empty, &mutex);
}
buffer[count++] = i;
printf("Produced: %d\n", i);
pthread_cond_signal(&cond_full);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond_full, &mutex);
}
int value = buffer[--count];
printf("Consumed: %d\n", value);
pthread_cond_signal(&cond_empty);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t prod_thread, cons_thread;
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
return 0;
}
在这个例子中,我们有一个缓冲区和一个互斥量来保护它。生产者和消费者线程都会检查缓冲区的状态,并在缓冲区满或空时等待条件变量的信号。当生产者放入数据时,它会唤醒消费者(如果它在等待)。反之亦然,当消费者消费数据时,它会唤醒生产者。这确保了生产者和消费者之间的同步和协调。