【Linux】线程的创建、回收分离以及线程的同步互斥

本文详细介绍了多线程编程的基础,包括线程创建、线程安全的实现(如互斥锁和条件变量),以及线程回收方法(join和detach)。重点讲解了如何使用互斥锁防止竞态条件,以及如何通过条件变量实现线程间的同步和避免忙等待。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、多线程的基本编程

二、线程安全(同步互斥)

1.使用互斥锁达到互斥

2.使用互斥锁和条件变量达到线程间的同步互斥


一、多线程的基本编程

        线程回收:线程在运行时需要分配内存空间、处理器时间等系统资源,这些资源在线程结束后应当被释放,以便其他线程或进程能够继续使用它们。如果不回收线程,就会导致系统资源的浪费和资源泄漏问题。

1.join回收(常用)

        使用 pthread_join 函数可以实现线程的等待和回收效果。具体来说,调用 pthread_join 函数会使当前线程(通常是主线程)等待指定的线程结束执行,然后将其资源回收。就是主线程在pthread_join 调用期间会阻塞,直到被等待的线程结束。

        当调用 pthread_join 后,主线程阻塞直到指定的线程结束。这意味着主线程会等待被等待线程执行完成后再继续执行后续的代码。如果不使用 pthread_join,主线程可能会在被等待线程执行完之前就结束,这样被等待线程的资源可能无法得到回收造成僵尸线程,可能导致资源泄漏。

        因此,使用 pthread_join 函数可以确保线程的资源在合适的时候被回收,保证程序的正确性和稳定性。

  1. pthread_create: 这个函数用于创建一个新线程。它接受四个参数:第一个参数是指向线程标识符的指针,用于存储新线程的标识符;第二个参数是用于设置线程属性的指针,通常为 NULL,表示使用默认属性;第三个参数是一个函数指针,指向我们想要在线程中执行的函数;第四个参数是传递给线程函数的参数。

  2. pthread_join: 这个函数用于等待指定的线程结束执行。它接受两个参数:第一个参数是要等待的线程的标识符;第二个参数是一个指向线程返回值的指针。在这个示例中,我们传递了 NULL,表示我们不关心线程的返回值。

  3. pthread_exit: 这个函数用于终止调用它的线程。在线程函数中,当线程完成任务后,应该使用 pthread_exit 来终止线程。

  4. 通常情况下,线程在结束时会调用 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.  使用分离线程回收
 

//thread: 要被标记为分离状态的线程的线程 ID,类型为 pthread_t
int pthread_detach(pthread_t thread);

        也可以通过调用 pthread_detach 函数将线程标记为“分离”的。这样,线程在结束时会自动释放资源,而不需要显式地调用 pthread_join 来等待和回收。分离线程一般用于不需要等待其完成的情况,比如后台任务,不会阻塞主线程。

        pthread_detach 将线程设置为分离状态,意味着该线程在完成执行后会自动释放其资源,而不需要其他线程调用 pthread_join 来等待和回收。分离状态的线程无法被其他线程等待和回收,因此它们在后台执行,并且不会产生僵尸线程。

        需要注意的是,一旦线程被设置为分离状态,就不能再将其恢复为非分离状态。因此,在调用 pthread_detach 之前,需要确保这个线程不再需要被其他线程等待和回收。

        主线程的结束:对分离线程的运行和生命周期没有直接影响,分离线程会继续执行,直到它自己完成任务或被取消。
        进程的结束:会终止所有线程,包括已分离的线程。分离线程独立于主线程,但仍受限于进程的生命周期。

        tips:主线程在最后调用了return,那么主线程结束,进程就会结束, 所有线程也会结束。如果没有return, 那么主线程结束不等于进程等结束。

        所以使用 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 函数通常在需要动态创建和销毁线程属性对象时使用。它的使用频率相对较低,因为大多数情况下,线程属性对象是在函数内部或者全局定义的静态对象,不需要销毁。

//指向一个线程属性对象的指针,该对象是通过 pthread_attr_init 初始化的
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)的发生。

        想象一下你和朋友在玩一个游戏,这个游戏有一把钥匙,你们两个需要轮流使用这把钥匙。问题是,如果你们两个同时试图拿起钥匙,可能会发生什么?或者当一个人正在使用钥匙的时候,另一个人也想拿走钥匙会发生什么?

        这个时候,互斥锁就像是一把只有一个人能够拿起的钥匙。当你需要使用这个共享资源(比如说一个全局变量、一个文件等)时,你会先尝试去获取这把互斥锁(也就是拿起钥匙)。如果没有其他人正在使用它,你就可以顺利地获取到互斥锁(拿到钥匙),然后就可以安全地使用这个共享资源了。一旦你使用完毕,你会把互斥锁(钥匙)放回原处,让其他人可以继续使用。

        但是,如果另一个人已经拿着这把互斥锁(钥匙),那么你就需要等待,直到他释放出互斥锁。这就保证了在同一时刻,只有一个线程可以访问共享资源,避免了数据的混乱和不一致性。

使用流程:

初始化互斥锁

        (1)在使用互斥锁之前,需要先初始化互斥锁。可以使用 pthread_mutex_init 函数进行初始化:



/*参数
pthread_mutex_t *mutex:

    指向一个 pthread_mutex_t 类型的互斥锁对象的指针。这个对象将在初始化后被用于线程同步。

const pthread_mutexattr_t *attr:
    指向一个 pthread_mutexattr_t 类型的互斥锁属性对象的指针。可以用来设置互斥锁的属性(例如,递归性、优先级继承等)。如果为 NULL,则使用默认属性。
*/

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

        (2)另外一种简便的方式来初始化互斥锁。这个宏提供了一个简单的方法来初始化互斥锁,而无需调用 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.使用互斥锁和条件变量达到线程间的同步互斥

        条件变量是一种用于线程间通信的同步机制,主要用于在线程等待某个条件满足时进行阻塞,以避免忙等待,从而提高系统效率。条件变量通常与互斥锁配合使用,用于保护共享资源的访问,并在共享资源状态发生变化时通知等待的线程。

  1. 避免忙等待: 在某些情况下,线程需要等待某个条件变为真,但不希望采用忙等待的方式不断地检查条件是否满足,以免占用过多的 CPU 资源。条件变量可以让线程在条件不满足时进入阻塞状态,直到条件被满足时才唤醒线程。

  2. 提高系统效率: 使用条件变量可以降低系统资源的消耗,因为线程在等待条件变量时会进入休眠状态,不再占用 CPU 资源,从而提高了系统的整体效率。

/*
功能:初始化一个条件变量。
参数:
cond:指向条件变量的指针。
attr:条件变量的属性,可以为 NULL,表示使用默认属性。
返回值:成功返回 0,失败返回错误码。
*/

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
/*
功能:使当前线程在条件变量上等待,直到其他线程发送通知或信号。
参数:
cond:指向条件变量的指针。
mutex:与条件变量相关联的互斥锁。在调用此函数时,线程会自动释放互斥锁,并在等待期间阻塞。
返回值:成功返回 0,失败返回错误码。
注意:当线程被唤醒时,互斥锁会被重新加锁,因此在退出 pthread_cond_wait 后,可以安全地继续访问共享资源。
*/

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/*
功能:唤醒一个在条件变量上等待的线程。
参数:
cond:指向条件变量的指针。
返回值:成功返回 0,失败返回错误码。
注意:如果有多个线程在等待条件变量,pthread_cond_signal 只会唤醒其中一个线程。如果需要唤醒所有等待线程,可以使用 pthread_cond_broadcast。
*/
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_init:
        //功能:初始化条件变量。
        //用法:在使用条件变量之前,首先调用这个函数来确保条件变量处于可用状态。
pthread_mutex_lock:
        //功能:锁定互斥锁。
        //用法:在访问共享资源之前,使用互斥锁来保证对资源的独占访问。
pthread_cond_wait:
        //功能:让线程等待条件满足。
       //用法:在线程需要等待条件时调用,释放互斥锁并进入等待状态,直到其他线程通知并且条件满足时才继续执行。意思是让线程进入等待状态,直到特定条件变成真,等别人通知它
pthread_cond_signal 或 pthread_cond_broadcast:
        //功能:唤醒等待条件变量的线程。
        //用法:当条件发生变化时,调用这两个函数之一来通知等待的线程。pthread_cond_signal 唤醒一个线程,而 pthread_cond_broadcast 唤醒所有等待的线程。
pthread_mutex_unlock:
        //功能:解锁互斥锁。
        //用法:完成对共享资源的访问后,解锁互斥锁,以便其他线程可以访问资源。
pthread_cond_destroy:
        //功能:销毁条件变量。
        //用法:在条件变量不再需要时调用,释放相关资源。

下面是一个基于互斥锁和条件变量的多线程示例,其中线程 A 负责生产数据,线程 B 负责消费数据:

        在这个示例中,两个线程共享一个缓冲区 bufferproducer 线程负责向缓冲区中生产数据,consumer 线程负责消费数据。通过互斥锁 mutex 和条件变量 cond 来保护缓冲区的访问,当缓冲区为空时,consumer 线程会进入等待状态,直到有数据被生产出来;当缓冲区已满时,producer 线程会进入等待状态,直到有数据被消费掉。

        这就是典型的生产者-消费者模型,其中消费者在缓冲区为空时会阻塞等待生产者的通知,而生产者在缓冲区已满时会阻塞等待消费者的通知。这种通过条件变量和互斥锁实现的同步机制,能够确保生产者和消费者之间的协调与同步,有效地避免了竞态条件和数据不一致的问题。

        其实也可以生产者一直生产,生产好了通知消费者,消费者不用通知生产者。消费者可以不需要通知生产者,因为生产者一直在生产数据。在这种情况下,消费者只需要等待生产者通知,当生产者生产好数据时,通知消费者进行消费即可。这种模型更符合实际生产消费的场景,因为生产者负责生产,而消费者负责消费,双方各司其职,相互协作,简化了线程间的通信和同步

下面这个例子是使用相互通知的,具体场景需要自己灵活选择

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> // for usleep function

#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); // 500毫秒
    }
    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); // 800毫秒
    }
    pthread_exit(NULL); // 退出线程
}

int main() 
{
    pthread_t producer_thread, consumer_thread;

    // 创建生产者线程
    if (pthread_create(&producer_thread, NULL, producer, NULL) != 0) 
    {
        fprintf(stderr, "Error creating producer thread\n");
        return 1;
    }

    // 创建消费者线程
    if (pthread_create(&consumer_thread, NULL, consumer, NULL) != 0) 
    {
        fprintf(stderr, "Error creating consumer thread\n");
        return 1;
    }

    // 等待生产者线程和消费者线程结束
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

放牛的守护神_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值