C语言多线程学习

本文详细介绍了线程的概念、线程在并发编程中的作用,包括线程互斥锁、临界区、进程同步和常见同步问题如生产者消费者问题、读者写者问题及哲学家进餐问题。通过实例展示了如何使用互斥锁和条件变量实现线程间的协调和同步。
摘要由CSDN通过智能技术生成
什么是线程?

**线程(Thread)是程序执行的最小单元,是操作系统进行调度和执行的基本单位。**一个线程可以看作是一个轻量级的进程,它独立地执行特定的任务或代码段,并拥有自己的程序计数器、寄存器集合和栈空间。

线程是操作系统提供的一种并发执行的机制,它允许在同一个程序中同时执行多个线程,每个线程都有自己的执行流和上下文信息。相对于多进程编程,多线程编程更加轻量级,线程之间的切换开销更小。

一个程序通常至少有一个主线程(Main Thread),它是程序的执行入口。主线程会按照顺序执行程序中的指令,可以创建和管理其他线程。除了主线程外,程序还可以创建额外的线程,这些线程可以并行执行不同的任务,或者在需要时进行协作。

线**程共享同一进程的资源,如内存空间、文件描述符和全局变量等。**这意味着多个线程可以访问和修改相同的数据,从而实现并发的数据处理和共享数据的通信。然而,由于线程之间的并发执行,必须谨慎处理线程之间的同步和互斥,以避免竞态条件和数据不一致性的问题。

线程的优点包括:

  • 并发性:多个线程可以同时执行不同的任务,提高程序的并发性和响应性。
  • 资源共享:线程可以共享进程的资源,避免资源的重复分配和浪费。
  • 轻量级:相对于进程来说,线程的创建和切换开销更小。

然而,线程编程也面临一些挑战,如线程同步、数据共享和竞态条件等问题。合理地管理和同步线程之间的操作是多线程编程中需要注意的关键点。

线程的同步互斥机制
1.线程互斥锁

在多线程中,所有的线程共用同一个全局变量,但是多个线程在操作同一个变量的时候,就可能会出现冲突的现象,内核为了解决这一问题引入线程的互斥锁,获取到锁的线程可以操作变量,获取不到锁资源的线程阻塞等待,没有执行的先后顺序,谁抢占锁成功谁执行。

创建互斥锁

互斥锁的类型是 pthread_mutex_t ,所以定义一个变量就是创建了一个互斥锁并初始化:

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁
pthread_mutex_init()

使用这个互斥锁,需要对互斥锁进行初始化, 使用函数 pthread_mutex_init() 对互斥锁进行初始化操作,未进行初始化,则创建的互斥锁不会生效:
//第二个参数为 NULL,互斥锁的属性会设置为默认属性

pthread_mutex_init(&mtx, NULL);
pthread_create 函数

创建一个线程

int WINPTHREAD_API pthread_create(pthread_t *th, const pthread_attr_t *attr, void ( func)(void *), void *arg);

函数参数的解释如下:

  • th 是一个指向 pthread_t 类型的指针,用于存储新创建的线程的标识符。
  • attr 是一个指向 pthread_attr_t 类型的指针,用于指定新线程的属性。可以使用默认属性,传递 NULL
  • func 是一个指针,指向一个函数,该函数将在新线程中执行。该函数应该具有 void * 类型的参数,并返回 void * 类型的值。这个函数可以执行线程所需的任何操作。
  • arg 是一个指针,传递给 func 函数作为参数。
pthread_join 函数
int WINPTHREAD_API pthread_join(pthread_t t, void **res);

用于等待指定的线程终止,并检索线程的返回值。

  • t 是一个 pthread_t 类型的变量,表示要等待的线程的标识符。
  • res 是一个指向指针的指针,用于存储线程的返回值。线程的返回值是通过这个指针间接传递的。

函数的返回类型是 int,表示函数的执行结果。如果成功等待线程的终止,返回值为 0;如果发生错误,返回一个非零的错误代码。

这个函数的作用是等待指定的线程终止。它会暂停当前线程的执行,直到指定的线程完成执行。在等待期间,当前线程将阻塞,直到指定的线程终止为止。

当目标线程终止时,pthread_join 函数将线程的返回值存储在 res 指向的指针中。通过这种方式,可以获取目标线程的返回值,并在需要时进行处理。

Simple 执行结果不是交替的,多循环几次就看到了
//
// Created by Kunsir on 2023/8/7.
//
#include "stdio.h"
#include "pthread.h"
#include "string.h"

void *Thread1(void *args)
{
    for(int  i = 0; i < 1000; i ++)
    {
        printf("哈哈哈哈\n");
    }

    return "Thread1 执行成功";
}

void *Thread2(void *args)
{
    for(int  i = 0; i < 1000; i ++) {
        printf("嘻嘻嘻嘻\n");
    }
    return "Thread2 执行成功";
}

int main()
{
    int res;
    pthread_t myThread1, myThread2;
    void *threadResult;
    res = pthread_create(&myThread1, NULL, Thread1, NULL);
    if (res) {
        printf("线程 1 创建失败\n");
        return 0;
    }

    res = pthread_create(&myThread2, NULL, Thread2, NULL);
    if (res){
        printf("线程 2 创建失败\n");
        return 0;
    }
    // 阻塞主线程直到 myThread1 线程执行结束,threadResult 用来接收返回值,阻塞状态才解除
    res = pthread_join(myThread2, &threadResult);
    printf("%s\n", (char*)threadResult);
    // 阻塞主线程直到 myThread2 线程执行结束,threadResult 用来接收返回值,阻塞状态才解除
    res = pthread_join(myThread1, &threadResult);
    printf("%s\n", (char*)threadResult);
    printf("主程序执行完毕\n");
    return 0;
}
进程同步的概念
  1. 临界资源:在系统中有许多硬件和软件资源,如打印机、公共变量等,这些资源在一段时间内只允许一个进程访问或者使用,这种资源称之为临界资源。
  2. 临界区:作为临界资源,不论硬件临界资源还是软件临界资源,多个并发的进程都必须互斥地访问或者使用,这时候,把每个进程访问临界资源的那段代码称为临界区。
  3. 进程同步:进程同步是指多个相关进程在执行次序上的协调这些进程相互合作,在一些关键点上需要相互等待或者通信。通过临界区可以协调进程间的合作关系,这就是同步。
  4. 进程互斥:进程互斥是指当一个程序进入临界区使用临界资源时,另一个进程必须等待。当占用临界资源的进程退出临界区后,另一个进程才被允许使用临界资源。通过临界区可以协调程序间资源共享关系,就是进程互斥。进程互斥是同步的一种特例。

常用的线程上锁函数

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

pthread_mutex_lock

pthread_mutex_lock 是 POSIX 线程库中的一个函数,用于获取互斥锁(mutex lock)。互斥锁是一种同步机制,用于保护共享资源,确保在任何给定时间内只有一个线程可以访问被保护的区域。

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:指向互斥锁的指针,用于对共享资源进行保护和同步。

pthread_mutex_lock 的工作流程如下:

  1. 如果互斥锁当前没有被其他线程占用,则线程成功获取互斥锁,继续执行后续代码。获取互斥锁的线程被称为拥有者。
  2. 如果互斥锁当前被其他线程占用,则线程将被阻塞,直到互斥锁被释放。线程将进入休眠状态,不会占用 CPU 资源。
  3. 一旦互斥锁被释放,被阻塞的线程将被唤醒,并且其中一个线程将成功获取互斥锁,成为新的拥有者。

互斥锁的作用是确保在任何给定时间内只有一个线程可以访问被保护的区域。当一个线程获得互斥锁后,其他线程将被阻塞,直到拥有互斥锁的线程释放锁。这样可以避免多个线程同时访问共享资源,从而保证数据的一致性和线程安全性。

需要注意的是,对于每个互斥锁,只有拥有它的线程才能释放它。一般情况下,线程应该在完成对共享资源的访问后调用 pthread_mutex_unlock 函数来释放互斥锁,以便其他线程可以获取它。

使用互斥锁时,应该遵循以下准则:

  • 在访问共享资源之前,获取互斥锁。
  • 访问共享资源。
  • 在完成共享资源的访问后,释放互斥锁,以便其他线程可以获取它。

这样,通过互斥锁的获取和释放,可以保证共享资源的安全访问和同步。

pthread_mutex_unlock

pthread_mutex_unlock 是 POSIX 线程库中的一个函数,用于释放互斥锁(mutex lock)。互斥锁是一种同步机制,用于保护共享资源,确保在任何给定时间内只有一个线程可以访问被保护的区域。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:指向互斥锁的指针,需要被释放。

pthread_mutex_unlock 的工作流程如下:

  1. 当调用线程拥有互斥锁时,调用 pthread_mutex_unlock 函数将互斥锁释放。
  2. 一旦互斥锁被释放,其他线程可以通过调用 pthread_mutex_lock 来尝试获取互斥锁。

互斥锁的释放是为了允许其他线程获取它,以便访问共享资源。当一个线程完成对共享资源的访问后,应该调用 pthread_mutex_unlock 来释放互斥锁,以便其他线程可以获取它。

需要注意的是,只有拥有互斥锁的线程才能调用 pthread_mutex_unlock 来释放锁。如果一个线程尝试释放一个它没有拥有的互斥锁,行为是未定义的。

使用互斥锁时,应该遵循以下准则:

  • 在访问共享资源之前,获取互斥锁。
  • 访问共享资源。
  • 在完成共享资源的访问后,释放互斥锁,以便其他线程可以获取它。

通过互斥锁的获取和释放,可以保证在任何给定时间内只有一个线程能够访问共享资源,从而实现数据的一致性和线程安全性。

pthread_cond_wait()

用于阻塞当前线程,等待别的线程使用pthread_cond_signal()pthread_cond_broadcast来唤醒它 pthread_cond_wait() 必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex

pthread_cond_signal

函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。

但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续
wait,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()使用while循环来做条件判断.

生产者消费者问题
//
// Created by Kunsir on 2023/8/8.
//
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define BUFFER_SIZE 2

int buffer[BUFFER_SIZE];
int count = 0;
int in = 0;
int out = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t full = PTHREAD_COND_INITIALIZER;

void *producer(void *arg) {
    int i;
    for (i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
//        sleep(1);
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&empty, &mutex);
        }
        buffer[in] = i;
        in = (in + 1) % BUFFER_SIZE;
        count++;
        printf("Producer produced item: %d\n", i);
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&full);
    }
    pthread_exit(NULL);
}

void *consumer(void *arg) {
    int i;
    for (i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
//        sleep(1);
        while (count == 0) {
            pthread_cond_wait(&full, &mutex);
        }
        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;
        printf("Consumer consumed item: %d\n", item);
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&empty);
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t producerThread, consumerThread;

    pthread_create(&producerThread, NULL, producer, NULL);
    pthread_create(&consumerThread, NULL, consumer, NULL);

    pthread_join(producerThread, NULL);
    pthread_join(consumerThread, NULL);

    return 0;
}
三个经典同步问题
a.生产者-消费者(缓冲区问题)

img

产者一消费者问题(producer-consumerproblem)是指若干进程通过有限的共享缓冲区交换数据时的缓冲区资源使用问题。假设“生产者”进程不断向共享缓冲区写人数据(即生产数据),而“消费者”进程不断从共享缓冲区读出数据(即消费数据);共享缓冲区共有n个;任何时刻只能有一个进程可对共享缓冲区进行操作。所有生产者和消费者之间要协调,以完成对共享缓冲区的操作。

/*生产者进程结构:*/
do{  
     wait(empty) ;  
     wait(mutex) ;  
      
     add nextp to buffer  
      
     signal(mutex) ;  
     signal(full) ;  
}while(1) ;
 
/*消费者进程结构:*/
do{  
     wait(full) ;  
     wait(mutex) ;  
      
     remove an item from buffer to nextp  
      
     signal(mutex) ;  
     signal(empty) ;  
}while(1) ;
b.作者读者问题

读者一写者问题(readers-writersproblem)是指多个进程对一个共享资源进行读写操作的问题。

假设“读者”进程可对共享资源进行读操作,“写者”进程可对共享资源进行写操作;任一时刻“写者”最多只允许一个,而“读者”则允许多个。即对共享资源的读写操作限制关系包括:“读—写,互斥、“写一写”互斥和“读—读”允许。

img

c.哲学家进餐问题
  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

kunsir_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值