一文搞定之C语言多线程

前言:

        本文在Qt环境中编写的示例代码,需要在.pro文件中添加下行来链接 pthread 库:

LIBS += -pthread

 一、 C语言线程编程常用函数

1、创建线程

函数原型:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

参数说明:

  • thread:指向 pthread_t 类型的指针,用于存储新线程的标识符。
  • attr:指向 pthread_attr_t 类型的指针,用于指定新线程的属性。可以传入 NULL,表示使用默认属性。
  • start_routine:指向线程函数的指针,新线程将从这个函数开始执行,函数的返回值和参数类型都必须是 void *。
  • arg:传递给线程函数的参数,类型为 void *。

返回值:

  • 如果成功创建了新线程,返回值为 0。
  • 如果出现错误,返回错误码。

示例用法:

#include <stdio.h>
#include <pthread.h>

void* myFunction(void* arg) {
    int param = *(int*)arg;
    printf("This is a thread! Parameter = %d\n", param);
    return NULL;
}

int main() {
    pthread_t myThread;
    int param = 42;

    if(pthread_create(&myThread, NULL, myFunction, &param) != 0) {
        printf("Failed to create thread\n");
        return 1;
    }

    // 等待新线程执行结束
    pthread_join(myThread, NULL);

    printf("Main thread exiting\n");

    return 0;
}

 执行结果:

 2、回收线程

函数原型:

int pthread_join(pthread_t thread, void **retval);

参数说明:

  • thread:指定要等待的线程的标识符。
  • retval:指向指针的指针,用于获取线程的返回值。如果不关心线程的返回值,可以传入 NULL。

返回值:

  •     如果成功等待到线程结束,返回值为 0。
  •     如果出现错误,返回错误码。

示例用法:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数,接收一个整数参数并返回一个指针
void* thread_function(void* arg)
{
    int value = *(int*)arg;
    int result = value * value;
    // 通过动态内存分配创建一个整数指针,用于存储线程的结果
    int* thread_result = (int*)malloc(sizeof(int));
    *thread_result = result;
    // 返回结果指针
    pthread_exit(thread_result);

    return NULL;
}

int main() {
    pthread_t thread_id;
    int input_value = 5;
    // 创建新线程,将input_value作为参数传递给线程函数
    if (pthread_create(&thread_id, NULL, thread_function, (void*)&input_value) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束并获取结果
    void* thread_result;
    if (pthread_join(thread_id, &thread_result) != 0) {
        perror("pthread_join");
        exit(EXIT_FAILURE);
    }

    // 打印线程的结果
    int* result_ptr = (int*)thread_result;
    printf("Thread result: %d\n", *result_ptr);

    // 释放动态内存
    free(thread_result);

    return 0;
}

执行结果:

注意:pthread_join() 的参数 thread 必须是一个可等待的线程,否则会返回错误。另外,如果线程已经被分离(detached),则不需要调用 pthread_join()。

3、结束线程

        pthread_exit(void *retval) 函数可以用于指定线程的退出状态,而这个退出状态可以被其他线程使用 pthread_join 来回收。通常情况下,这两个函数是配合使用的。

函数原型:

void pthread_exit(void *retval);

参数说明:

  • retval:指定线程的返回值,类型为 void*

返回值:无。

示例用法:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void* thread_function(void* arg) {
    int thread_id = *(int*)arg;
    printf("Thread %d started\n", thread_id);

    // 模拟线程工作
    for (int i = 0; i < 5; i++) {
        printf("Thread %d working... %d\n", thread_id, i);
        sleep(1);
    }

    int* result = malloc(sizeof(int));
    *result = thread_id * 2;

    printf("Thread %d finished\n", thread_id);
    pthread_exit(result); // 退出并返回结果
}

int main() {
    pthread_t thread;
    int thread_id = 1;

    // 创建线程
    if (pthread_create(&thread, NULL, thread_function, &thread_id) != 0) {
        fprintf(stderr, "Failed to create thread\n");
        return 1;
    }

    printf("Main thread waiting for the child thread to finish...\n");

    void* result;

    // 等待子线程结束并获取结果
    if (pthread_join(thread, &result) != 0) {
        fprintf(stderr, "Failed to join thread\n");
        return 1;
    }

    printf("Child thread returned: %d\n", *(int*)result);
    free(result); // 释放结果内存

    printf("Main thread finished\n");

    return 0;
}

执行结果:

 思考:为什么在线程函数void* thread_function(void* arg)中,对于pthread_exit(void *retval)的参数result要采取在堆上分配内存呢?

        当线程退出时,pthread_exit 被调用,它会将指定的参数(无论是局部变量还是其他类型的变量)传递给等待该线程的 pthread_join 函数,并将控制权返回给等待线程的调用者。在这个过程中,线程函数所使用的栈帧会被销毁,因此局部变量的内存将会被回收。
        如果线程函数退出后,等待线程的 pthread_join 函数尝试访问已经被回收的局部变量,则会引发未定义的行为,可能导致程序崩溃或产生错误的结果。
        因此,为了确保安全性和正确性,建议使用动态分配的内存(例如使用 malloc 或 calloc 分配的内存)来存储需要作为退出状态传递的数据,并在不再需要时手动进行释放(使用 free 函数)。
        也可以使用全局变量或静态变量来存储退出状态,因为它们的生命周期会持续到整个程序的执行结束。这样,等待线程可以安全地访问这些变量。

4、线程取消

函数原型:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数说明:

  • thread:表示要取消的线程的标识符(pthread_t类型)。

返回值:

  • 函数返回0表示成功。
  • 返回非0值表示失败。

示例代码:

#include <stdio.h>
#include <pthread.h>

void* thread_function(void* arg) {
    printf("Thread started.\n");
    
    // 进入取消点前检查取消请求
    pthread_testcancel();
    
    // 模拟线程工作
    for (int i = 0; i < 5; i++) {
        printf("Working...%d\n", i);
        // 模拟长时间运行的任务
        sleep(1);
        
        // 进入取消点前检查取消请求
        pthread_testcancel();
    }
    
    printf("Thread finished.\n");
    return NULL;
}

int main() {
    pthread_t thread_id;
    
    // 创建新线程
    if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }
    
    // 模拟一段时间后取消线程
    sleep(3);
    if (pthread_cancel(thread_id) != 0) {
        perror("pthread_cancel");
        return 1;
    }
    
    // 等待线程结束
    if (pthread_join(thread_id, NULL) != 0) {
        perror("pthread_join");
        return 1;
    }
    
    printf("Main thread finished.\n");
    return 0;
}

执行结果:

注意事项:

  • 1、pthread_cancel并不保证立即终止目标线程的执行。线程是否真正取消取决于其自身的实现和逻辑,在收到取消请求后,线程可以选择忽略请求并继续执行,或者调用pthread_exit函数主动退出线程。
  • 2、要正确使用pthread_cancel函数,需要确保目标线程是可取消的。可以通过以下方法创建可取消的线程:
          a、在创建线程时,使用pthread_setcancelstate函数将线程的取消状态设置为PTHREAD_CANCEL_ENABLE,表示线程可以被取消。
          b、在需要取消点(Cancellation Point)的位置,使用pthread_testcancel函数检查是否收到取消请求,如果收到请求,则可以选择执行清理操作并调用pthread_cancel函数取消线程。

 5、线程分离

   pthread_detach是一个函数,用于分离指定的线程。分离线程意味着当线程结束时,它的资源将会自动被回收,而无需显式的调用pthread_join

函数原型:

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数说明:

  • thread:表示要分离的线程的标识符(pthread_t类型)。

返回结果:

  • 函数返回0表示成功。
  • 返回非0值表示失败。

示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* thread_function(void* arg) {
    printf("Thread started.\n");

    // 模拟线程工作
    sleep(3);

    printf("Thread finished.\n");
    return NULL;
}

int main() {
    pthread_t thread_id;

    // 创建新线程
    if (pthread_create(&thread_id, NULL, thread_function, NULL) != 0) {
        perror("pthread_create");
        return 1;
    }

    // 分离线程
    if (pthread_detach(thread_id) != 0) {
        perror("pthread_detach");
        return 1;
    }

    sleep(4); // 等待足够长的时间以观察线程的工作

    printf("Main thread finished.\n");
    return 0;
}

 执行结果:

 注意事项:

  • pthread_detach函数必须在目标线程结束之前调用,否则无法生效。也就是说,该函数仅在目标线程尚未结束时调用才会起作用。

二、C语言线程间的同步互斥机制

1、mutex(互斥锁)

        C语言中的mutex(互斥锁)是一种线程同步机制,用于保护临界区,确保同一时间只有一个线程访问被保护的资源。在C语言中,可以通过pthread库提供的mutex相关函数来实现互斥锁。

在使用互斥锁时,常用的函数包括:

  1. pthread_mutex_init:初始化互斥锁,将互斥锁设置为可用状态。
  2. pthread_mutex_lock:加锁,尝试获取互斥锁,如果互斥锁已经被其他线程占用,则调用线程会被阻塞,直到互斥锁可用。
  3. pthread_mutex_unlock:解锁,释放互斥锁,让其他线程可以获取互斥锁。
  4. pthread_mutex_destroy:销毁互斥锁,释放相关资源。

使用互斥锁的基本流程如下:

  1. 在需要保护的临界区前调用pthread_mutex_lock加锁,确保只有一个线程可以进入临界区。
  2. 执行临界区代码,对共享资源进行操作。
  3. 在临界区结束前调用pthread_mutex_unlock解锁,释放互斥锁。
  4. 其他线程可以通过pthread_mutex_lock加锁以访问同一份资源。

        互斥锁的使用可以有效避免多个线程对共享资源的并发访问引发的竞态条件问题,保证线程间的正确协作和资源的正确访问。

示例代码:

#include <stdio.h>
#include <pthread.h>
#include <stdint.h>  // 引入 intptr_t 类型

int counter = 0;  // 共享的计数器
pthread_mutex_t mutex;  // 互斥锁

void* increment_counter(void* thread_id) {
    intptr_t tid = (intptr_t)thread_id;  // 使用 intptr_t 来存储指针值

    pthread_mutex_lock(&mutex);  // 加锁

    counter++;  // 计数器增加
    printf("Thread %ld: Counter value is %d\n", tid, counter);

    pthread_mutex_unlock(&mutex);  // 解锁

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[5];  // 5个线程

    pthread_mutex_init(&mutex, NULL);  // 初始化互斥锁

    for (intptr_t i = 0; i < 5; i++) {
        pthread_create(&threads[i], NULL, increment_counter, (void*)i);  // 创建线程,并传递线程编号
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);  // 等待线程结束
    }

    pthread_mutex_destroy(&mutex);  // 销毁互斥锁

    return 0;
}

执行结果:

2、信号量(Semaphore)

        C语言中的信号量(Semaphore)是一种线程同步机制,用于控制对共享资源的访问。在C语言中,可以通过信号量实现线程的互斥和同步。C语言标准库中没有直接提供信号量的实现,但可以使用第三方库(如POSIX Threads库)来实现信号量。

信号量的基本操作包括:

  1. sem_init:初始化信号量,设置信号量的初始值。
  2. sem_wait:等待信号量,如果信号量的值大于0,则将信号量的值减1并继续执行;如果信号量的值为0,则等待直到有其他线程释放信号量。
  3. sem_post:释放信号量,将信号量的值加1,唤醒一个或多个等待的线程。
  4. sem_destroy:销毁信号量,释放相关资源。

使用信号量的基本流程如下:

  1. 在需要进行同步的地方调用sem_wait等待信号量,确保只有满足某个条件后才能继续执行。
  2. 当条件满足时,通过sem_post释放信号量,唤醒等待的线程继续执行。

示例代码:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdint.h>  // 引入 intptr_t 类型

#define NUM_THREADS 5

int counter = 0;
sem_t semaphore;

void* increment_counter(void* thread_id) {
    intptr_t tid = (intptr_t)thread_id;

    // 等待信号量
    sem_wait(&semaphore);

    printf("Thread %ld: Incrementing counter.\n", tid);
    counter++;
    printf("Thread %ld: New counter value: %d\n", tid, counter);

    // 释放信号量
    sem_post(&semaphore);

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];

    // 初始化信号量
    sem_init(&semaphore, 0, 1);

    for (long i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, increment_counter, (void*)i);
    }

    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁信号量
    sem_destroy(&semaphore);

    printf("Final counter value: %d\n", counter);

    return 0;
}

执行结果:

3、条件变量(Condition Variable)

        C语言中的条件变量(Condition Variable)是一种线程同步机制,用于实现线程之间的通信和同步。条件变量允许线程在某个条件满足时等待,以及在满足条件时通知其他等待的线程。

        C语言标准库中没有直接提供条件变量的实现,但可以使用第三方库(如POSIX Threads库)来实现条件变量。

条件变量的基本操作包括:

  1. pthread_cond_init:初始化条件变量。
  2. pthread_cond_wait:等待条件变量,将当前线程阻塞,直到其他线程调用pthread_cond_signalpthread_cond_broadcast通知条件满足。
  3. pthread_cond_signal:唤醒至少一个等待的线程。
  4. pthread_cond_broadcast:唤醒所有等待的线程。
  5. pthread_cond_destroy:销毁条件变量。

使用条件变量的基本流程如下:

  1. 在需要进行同步的地方,使用互斥锁(pthread_mutex_t)保护共享数据的访问。
  2. 当条件不满足时,调用pthread_cond_wait等待条件变量。
  3. 当其他线程满足了条件,通过pthread_cond_signalpthread_cond_broadcast唤醒等待的线程。
  4. 被唤醒的线程从pthread_cond_wait返回,重新获取互斥锁,并检查条件是否满足,直到满足条件后继续执行。

 示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 6

int buffer[BUFFER_SIZE];
int buffer_count = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t buffer_empty_condition = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_full_condition = PTHREAD_COND_INITIALIZER;

void* producer(void* arg) {
    for (int i = 0; i < BUFFER_SIZE * 2; i++) {
        pthread_mutex_lock(&mutex);

        // 等待缓冲区不满
        while (buffer_count == BUFFER_SIZE) {
            pthread_cond_wait(&buffer_empty_condition, &mutex);
        }

        // 生产数据
        buffer[buffer_count] = i;
        buffer_count++;

        printf("Producer produced data: %d\n", i);

        // 唤醒消费者线程
        pthread_cond_signal(&buffer_full_condition);

        pthread_mutex_unlock(&mutex);
    }

    pthread_exit(NULL);
}

void* consumer(void* arg) {
    for (int i = 0; i < BUFFER_SIZE * 2; i++) {
        pthread_mutex_lock(&mutex);

        // 等待缓冲区不空
        while (buffer_count == 0) {
            pthread_cond_wait(&buffer_full_condition, &mutex);
        }

        // 消费数据
        int data = buffer[buffer_count-1];
        buffer_count--;

        printf("Consumer consumed data: %d\n", data);

        // 唤醒生产者线程
        pthread_cond_signal(&buffer_empty_condition);

        pthread_mutex_unlock(&mutex);
    }

    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;
}

 执行结果:

三、补充

  1. 屏障(Barrier):屏障用于控制多个线程在某个点处同步。当线程到达屏障时会被阻塞,直到所有线程都到达屏障后才会继续执行。

  2. 自旋锁(Spinlock):自旋锁是一种忙等待锁,线程如果发现自旋锁已经被其他线程占用时,会不断地进行忙等待,直到锁被释放。 

  3. 同步(Synchronization)

    • 同步是指多个进程或线程之间按照一定的顺序执行,以及在执行过程中相互协作和通信的过程。
    • 同步通常用于协调不同进程或线程之间的操作,确保它们按照一定的顺序执行,避免出现竞态条件等问题。
  4. 互斥(Mutual Exclusion)

    • 互斥是指多个进程或线程之间对共享资源的互斥访问,即在任意时刻只允许一个进程或线程访问共享资源,其他进程或线程需要等待。
    • 互斥保证了共享资源的完整性和一致性,防止多个进程或线程同时访问共享资源而导致数据不一致或破坏资源的情况。
  5. 同步一定互斥: 在实现同步的过程中,通常需要保证对共享资源的互斥访问,以确保数据的一致性。因此,同步操作通常伴随着互斥操作,确保多个线程或进程能够按照既定顺序协作。

  6. 互斥不一定同步: 互斥指的是对共享资源的互斥访问,确保在任意时刻只有一个线程或进程访问共享资源。而并非所有互斥操作都需要同步。有些情况下,只需确保对共享资源的互斥访问而无需考虑线程之间的协作顺序。

  • 8
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值