-
1.进程与线程
-
资源共享:
- 进程:不同进程之间相互独立,拥有各自独立的资源(地址空间)和状态,进程间的通信需要专门的机制(如管道、消息队列等)。
- 线程:同一进程下的线程共享进程的资源,包括内存(堆区,代码段、全局变量)和文件描述符等,因此线程间通信更加方便,但同时需要注意同步问题以避免竞争条件。每个线程有自己独立的栈、寄存器。
-
系统开销:
- 进程:创建一个新的进程需要分配独立的资源,这通常比创建线程消耗更多的系统资源和时间。
- 线程:由于线程共享其所属进程的资源,创建线程的开销较小,切换成本也较低。
-
安全性与稳定性:
- 进程:由于进程之间的隔离性,一个进程崩溃通常不会直接影响到其他进程。
- 线程:因为线程共享进程资源,所以一个线程出现问题可能会导致整个进程崩溃或不稳定。
-
调度与切换:
- 操作系统负责进程的调度与切换;而线程既可以由操作系统内核调度,也可以通过用户态的线程库进行管理,具体取决于线程模型的设计。
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)是一种多线程同步机制,用于解决多线程并发读写共享资源的问题。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制特别适用于读操作远多于写操作的场景,因为它可以显著提高并发性能。
读写锁通常包含两种锁:
-
共享锁(Shared Lock):也称为读锁。当一个线程获得共享锁时,其他线程可以同时获得共享锁以读取共享资源,但不能获得排他锁。
-
排他锁(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;
}
注意事项
-
信号量的初始值:信号量的初始值应根据实际需求设置。例如,空闲槽位信号量的初始值应等于缓冲区大小,已填充槽位信号量的初始值应为0。
-
互斥信号量:在访问共享资源时,应使用互斥信号量保护临界区。
-
资源管理:在使用完毕后,必须销毁信号量以释放系统资源。
3.线程池
预先创建一定数量的线程,避免了频繁创建和销毁线程所带来的开销,同时提高系统的性能和响应速度
1. 线程池的参数(结构体)
-
核心(最小)线程数(corePoolSize):
-
通常设置为
CPU核心数 + 1
,适用于CPU密集型任务。 -
对于IO密集型任务,可以设置为
2 * CPU核心数
。
-
-
最大线程数(maximumPoolSize):
-
通常设置为一个较大的值,但要根据系统资源和任务类型进行调整。
-
-
任务队列(workQueue):
-
对于任务执行时间较短的场景,可以使用
SynchronousQueue
。 -
对于任务执行时间较长的场景,可以使用
ArrayBlockingQueue
或LinkedBlockingQueue
。
-
-
空闲线程存活时间(keepAliveTime):
-
对于需要快速响应的场景,可以设置较短的存活时间。
-
对于任务量不稳定的场景,可以设置较长的存活时间。
-
2.编写线程池函数
1.结构体:
线程池结构体(工作队列, 工作线程,管理线程, 锁,条件变量),工作队列结构体(函数指针,函数参数)
2.基本功能:
1.线程池初始化:
结构体初始化,创建线程
2.工作线程(消费者线程):
判断条件变量,处理工作队列,必要时(在管理线程管理下)结束线程
3.管理线程:
管理工作线程数目:忙碌线程数*2<存货线程数 杀死线程
当前线程数< 工作队列数,创建线程
4.添加工作队列(生产者):
根据条件变量进行沉睡,唤醒工作线程
5.线程退出:
根据线程ID, 结束线程
6.线程池销毁:
杀死管理线程,杀死工作线程,释放锁,释放资源