多线程和线程同步(c/c++)

  • 1.进程与线程

  1. 资源共享

    • 进程:不同进程之间相互独立,拥有各自独立的资源(地址空间)和状态,进程间的通信需要专门的机制(如管道、消息队列等)。
    • 线程:同一进程下的线程共享进程的资源,包括内存(堆区,代码段、全局变量)和文件描述符等,因此线程间通信更加方便,但同时需要注意同步问题以避免竞争条件。每个线程有自己独立的栈、寄存器。
  2. 系统开销

    • 进程:创建一个新的进程需要分配独立的资源,这通常比创建线程消耗更多的系统资源和时间。
    • 线程:由于线程共享其所属进程的资源,创建线程的开销较小,切换成本也较低。
  3. 安全性与稳定性

    • 进程:由于进程之间的隔离性,一个进程崩溃通常不会直接影响到其他进程。
    • 线程:因为线程共享进程资源,所以一个线程出现问题可能会导致整个进程崩溃或不稳定。
  4. 调度与切换

    • 操作系统负责进程的调度与切换;而线程既可以由操作系统内核调度,也可以通过用户态的线程库进行管理,具体取决于线程模型的设计。

 2. POSIX

在 Linux 中,C/C++ 使用 POSIX 线程(pthread)库来实现多线程编程。

POSIX(Portable Operating System Interface)是一个定义了操作系统接口的标准,其中的 POSIX 线程(POSIX Threads,简称 Pthreads)是用于多线程编程的标准化接口。

线程处理函数的返回值在pthread_exit传入,由主线程通过线程接收函数thread_join接收

 1.线程的创建与终止

1. 线程创建

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
  • 功能:创建一个新线程。

  • 参数

    • pthread_t *thread:指向线程标识符的指针,用于后续引用该线程。

    • const pthread_attr_t *attr:线程属性指针,用于设置线程的属性(如栈大小、分离状态等)。如果为 NULL,则使用默认属性。

    • void *(*start_routine)(void*):线程的启动例程,即线程将要执行的函数。

    • void *arg:传递给启动例程的参数。

  • 返回值:成功返回 0,失败返回错误码。

2. 线程等待

int pthread_join(pthread_t thread, void **retval);
  • 功能:等待指定线程结束。

  • 参数

    • pthread_t thread:要等待的线程的标识符。

    • void **retval:指向一个指针的指针,用于存储线程的返回值。如果不需要返回值,可以传入 NULL

  • 返回值:成功返回 0,失败返回错误码。

3. 线程分离

int pthread_detach(pthread_t thread);
  • 功能:用于将线程设置为分离状态。分离状态的线程在终止时会自动释放其资源,而不需要其他线程调用 pthread_join 来回收资源

  • 参数

    • pthread_t thread:要设置为分离状态的线程的标识符。

  • 返回值:成功返回 0,失败返回错误码。

4. 线程取消

int pthread_cancel(pthread_t thread);
  • 功能:请求取消指定线程的执行。

  • 参数

    • pthread_t thread:要取消的线程的标识符。

  • 返回值:成功返回 0,失败返回错误码。

5. 线程退出

void pthread_exit(void* retval);
  • 功能:使当前线程退出,并返回一个值。

  • 参数

    • void* retval:线程的返回值

 2.线程同步

1.线程互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 功能

    • pthread_mutex_init:初始化互斥锁。

    • pthread_mutex_destroy:销毁互斥锁。

    • pthread_mutex_lock:锁定互斥锁。

    • pthread_mutex_unlock:解锁互斥锁。

2.POSIX读写锁函数

读写锁(Read-Write Lock)是一种多线程同步机制,用于解决多线程并发读写共享资源的问题。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制特别适用于读操作远多于写操作的场景,因为它可以显著提高并发性能。

读写锁通常包含两种锁:

  1. 共享锁(Shared Lock):也称为读锁。当一个线程获得共享锁时,其他线程可以同时获得共享锁以读取共享资源,但不能获得排他锁。

  2. 排他锁(Exclusive Lock):也称为写锁。当一个线程获得排他锁时,其他线程既不能获得共享锁也不能获得排他锁

在POSIX标准中,读写锁通过pthread_rwlock_t类型实现,相关操作函数如下:

1.初始化和销毁
int pthread_rwlock_init(pthread_rwlock_t *prwlock, const pthread_rwlockattr_t *prwlockattr);

初始化一个读写锁。prwlock是读写锁的指针,prwlockattr是读写锁属性对象的指针,可以为NULL。成功返回0,失败返回错误号。

int pthread_rwlock_destroy(pthread_rwlock_t *prwlock);

销毁一个读写锁,释放其占用的资源。成功返回0,失败返回错误号。

2.读锁操作
int pthread_rwlock_rdlock(pthread_rwlock_t *prwlock);

阻塞式获取读锁,允许多个线程同时持有读锁。成功返回0,失败返回错误号。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *prwlock);

非阻塞式尝试获取读锁。如果锁已被写锁占用,则立即返回错误号EBUSY

int pthread_rwlock_timedrdlock(pthread_rwlock_t *prwlock, const struct timespec *abs_timeout);

带超时时间的阻塞式获取读锁。abs_timeout为绝对超时时间。

3.写锁操作
int pthread_rwlock_wrlock(pthread_rwlock_t *prwlock);

阻塞式获取写锁,写锁是排他的。成功返回0,失败返回错误号。

int pthread_rwlock_trywrlock(pthread_rwlock_t *prwlock);

非阻塞式尝试获取写锁。如果锁已被读锁或写锁占用,则立即返回错误号EBUSY

int pthread_rwlock_timedwrlock(pthread_rwlock_t *prwlock, const struct timespec *abs_timeout);

带超时时间的阻塞式获取写锁。

4.解锁
int pthread_rwlock_unlock(pthread_rwlock_t *prwlock);

释放读写锁。成功返回0,失败返回错误号。

3.条件变量

条件变量是多线程编程中用于线程间同步的重要机制,通常与互斥锁配合使用,以实现线程在特定条件满足时的等待和唤醒 。条件变量->阻塞线程,互斥锁->线程同步

1. 初始化和销毁条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

2. 等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

如果在指定时间abstime内条件未满足,线程将解除阻塞。 

pthread_mutex_lock(&mutex);
while (条件不满足) {
    pthread_cond_wait(&cond, &mutex);
}
// 条件满足后执行相关操作
pthread_mutex_unlock(&mutex);

3. 通知条件变量
int pthread_cond_signal(pthread_cond_t *cond);
pthread_mutex_lock(&mutex);
// 修改共享数据,使条件满足
pthread_cond_signal(&cond); // 唤醒一个等待的线程
pthread_mutex_unlock(&mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);

4.信号量

信号量(Semaphore)通过维护一个计数器来控制对共享资源的访问,计数器的值表示当前可用的资源数量。信号量可以用于实现线程间的同步和互斥,常用于解决生产者-消费者问题、线程池任务调度等场景。

1. 初始化和销毁信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

功能:初始化一个信号量。sem是信号量的指针,pshared表示信号量是否在进程间共享(0表示不共享,非0表示在进程间共享),value是信号量的初始值。

int sem_destroy(sem_t *sem);

功能:销毁一个信号量,释放其占用的资源。

2. 信号量操作
int sem_wait(sem_t *sem);

功能:等待信号量。如果信号量的值大于0,则将其减1并继续执行;如果信号量的值为0,则当前线程阻塞,直到信号量的值大于0

sem_wait(&sem); // 等待信号量
int sem_trywait(sem_t *sem);

功能:尝试等待信号量。如果信号量的值大于0,则将其减1并继续执行;如果信号量的值为0,则立即返回错误码EAGAIN,而不阻塞线程。

int sem_post(sem_t *sem);

功能:释放信号量。将信号量的值加1,如果有线程正在等待该信号量,则唤醒一个线程。

以下是一个使用POSIX信号量的生产者-消费者问题的示例代码:

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

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int count = 0;

sem_t empty_slots; // 空闲槽位信号量
sem_t full_slots;  // 已填充槽位信号量
sem_t mutex;       // 互斥信号量

void* producer(void* arg) {
    for (int i = 0; i < 100; i++) {
        sem_wait(&empty_slots); // 等待空闲槽位
        sem_wait(&mutex);       // 进入临界区

        buffer[count++] = i;
        printf("Produced %d\n", i);

        sem_post(&mutex);       // 离开临界区
        sem_post(&full_slots);  // 增加已填充槽位
        sleep(1);
    }
    return NULL;
}

void* consumer(void* arg) {
    for (int i = 0; i < 100; i++) {
        sem_wait(&full_slots);  // 等待已填充槽位
        sem_wait(&mutex);       // 进入临界区

        printf("Consumed %d\n", buffer[--count]);

        sem_post(&mutex);       // 离开临界区
        sem_post(&empty_slots); // 增加空闲槽位
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t producer_thread, consumer_thread;

    // 初始化信号量
    sem_init(&empty_slots, 0, BUFFER_SIZE);
    sem_init(&full_slots, 0, 0);
    sem_init(&mutex, 0, 1);

    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);

    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);

    // 销毁信号量
    sem_destroy(&empty_slots);
    sem_destroy(&full_slots);
    sem_destroy(&mutex);

    return 0;
}

注意事项

  1. 信号量的初始值:信号量的初始值应根据实际需求设置。例如,空闲槽位信号量的初始值应等于缓冲区大小,已填充槽位信号量的初始值应为0。

  2. 互斥信号量:在访问共享资源时,应使用互斥信号量保护临界区。

  3. 资源管理:在使用完毕后,必须销毁信号量以释放系统资源。

3.线程池 

预先创建一定数量的线程,避免了频繁创建和销毁线程所带来的开销,同时提高系统的性能和响应速度 

1. 线程池的参数(结构体)

  • 核心(最小)线程数(corePoolSize)

    • 通常设置为CPU核心数 + 1,适用于CPU密集型任务。

    • 对于IO密集型任务,可以设置为2 * CPU核心数

  • 最大线程数(maximumPoolSize)

    • 通常设置为一个较大的值,但要根据系统资源和任务类型进行调整。

  • 任务队列(workQueue)

    • 对于任务执行时间较短的场景,可以使用SynchronousQueue

    • 对于任务执行时间较长的场景,可以使用ArrayBlockingQueueLinkedBlockingQueue

  • 空闲线程存活时间(keepAliveTime)

    • 对于需要快速响应的场景,可以设置较短的存活时间。

    • 对于任务量不稳定的场景,可以设置较长的存活时间。

2.编写线程池函数

1.结构体:

线程池结构体(工作队列, 工作线程,管理线程, 锁,条件变量),工作队列结构体(函数指针,函数参数) 

2.基本功能:
1.线程池初始化:

结构体初始化,创建线程

2.工作线程(消费者线程):

判断条件变量,处理工作队列,必要时(在管理线程管理下)结束线程

3.管理线程:

管理工作线程数目:忙碌线程数*2<存货线程数 杀死线程

当前线程数< 工作队列数,创建线程

4.添加工作队列(生产者):

根据条件变量进行沉睡,唤醒工作线程  

5.线程退出:

根据线程ID, 结束线程

6.线程池销毁:

杀死管理线程,杀死工作线程,释放锁,释放资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值