C++多线程与多进程编程
-
Linux下的多线程库:NGPT和NPTL,NPTL比linuxThread效率更高,且符合POSIX编程规范,因此通常都是用POSIX下的线程库:pthread标准
NPTL的实现包括三个内容,创建线程和结束线程;读取和设置线程属性;POSIX线程同步的方式:POSIX 信号量,互斥条件和条件变量。
-
线程模型
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体。根据调度者的身份,线程可以分为用户线程和内核线程两种
-
内核线程:运行在内核空间,由内核来调度
-
用户线程:运行在用户空间,由线程库来调度。
当进程的一个内核线程获得CPU的使用权时,他就加载并运行了一个用户线程,因此,内核线程相当于用户线程的运行“容器”,一个进程可以拥有M个内核线程和N个用户线程,M<=N。
根据这个比值,有三种情况:完全在用户空间实现多线程;完全在内核空间实现多线程;双层调度。
完全在用户空间实现的线程无需内核的支持,内核甚至不知道这些线程的存在。线程库负责管理所有的线程,使用longjmp来切换线程的执行,使他们看起来像是“并发”执行的,但实际上内核仍然是把整个进程作为最小单位来调度的,所以内核线程就是进程本身。
完全在内核空间实现的线程将创建,调度的任务都交给了内核,线程库无需执行管理任务。
-
-
Linux下的线程库最有名的就是LInuxThreads和NPTL,现在默认是NPTL。LinuxThreads线程库的内核线程是使用clone系统调用创建进程模拟的,clone系统调用和fork系统调用过程类似,不过可以指定创建的子进程与调用进程共享相同的虚拟地址空间。用进程来模拟线程有许多缺点:
-
每个线程拥有不同的PID
-
LInux信号处理函数是基于进程的,现在一个进程内所有线程都能且必须处理信号。
-
系统的最大线程数就是最大进程数
等
LinuxThreads还提供管理线程,负责接收系统发送的信号,终止线程,阻塞线程和回收线程堆栈等工作,效率低下。
NPTL库在linux内核的完善下应运而生,Linux内核提供了真正的内核线程,NPTL有如下优点:
-
内核线程不再是一个进程,进程的线程可以运行在不同的CPU上,由内核调度。
-
不存在管理线程,回收终止等工作都由内核完成
-
线程的同步由内核完成,隶属于不同进程的线程之间也能共享互斥锁,实现跨进程的同步。
-
-
线程创建
#include <pthread.h> int pthread_create(pthread_t *thread,const pthread_attr_t *attr,vid *(*start_routine)(void* ),void *arg);
pthread_t为一个无符号长整型,为线程的编号;arg参数用于设置线程的属性,NULL为默认。
该函数成功时返回0,失败返回错误码。
-
线程调度与停止
线程一旦创建好,内核就可以调度内核线程来执行start_routine函数指向的函数了。线程退出最好使用pthread_exit,确保安全,干净的退出。
#include <pthread.h> void pthread_exit(void *retval);//retval向回收者传递退出信息。
-
线程的回收
一个进程中的所有线程都可以通过调用pthread_join函数来回收其他线程,即等待其他线程结束,这类似于回收进程的wait和waitpid系统调用。
#include <pthread.h> int pthread_join(pthread_t thread,void *retval);
thread参数是目标的标识符,成功返回0,失败返回错误码:
EDEADLK:可能引起死锁,比如两个线程互相对对方调用pthread_join,或者线程对自身调用pthread_join EINVAL:目标线程是不可回收的,或者已经有其他线程在回收该线程 ESRCH:目标线程不存在
-
线程的终止与取消
一个线程可能出现异常,我们希望手动终止他,可以通过如下函数:
#include <pthread.h> int pthread_cancel(pthread_t thread);
成功返回0,失败返回错误码。
不过,接受到取消请求的目标线程可以决定是否允许取消以及如何取消,
#include <pthread.h> int pthread_setcancelstate(int state,int *oldstate); int pthread_setcanceltype(int type,int *oldtype);
这两个函数的第一参数分别设置线程的取消状态和取消类型;第二个参数则分别记录线程原来的取消状态和取消类型。更多细节请看API
-
线程的属性
#include <pthread.h> #define __SIZEOF_PTHREAD_ATTR_T 36 typedef union{ char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; }pthread_attr_t各种线程属性全部包含在这个字符数组中,可以通过一系列API来操作pthread_attr_t,以便获取和设置线程的属性:
1.pthread_attr_init 功能: 对线程属性变量的初始化。
2.pthread_attr_setscope 功能: 设置线程 __scope 属性...
3.pthread_attr_setdetachstate 功能: 设置线程detach...
4.pthread_attr_setschedparam 功能: 设置线程sched...
5.pthread_attr_getschedparam 功能: 得到线程优先级。
线程的属性如下:
typedef struct { int detachstate; //线程的分离状态 int schedpolicy; //线程的调度策略 struct sched schedparam;//线程的调度参数 int inheritsched; //线程的继承性 int scope; //线程的作用域 size_t guardsize; //线程栈末尾的警戒缓冲区大小 int stackaddr_set; //线程栈的设置 void* stackaddr; //线程栈的启始位置 size_t stacksize; //线程栈大小 }pthread_attr_t;都有相应的函数进行设置。
-
POSIX信号量:处理多线程的同步问题
3种处理先吃之间同步问题的机制:POSIX信号量,互斥量和条件变量
LInux中,信号量有两种,第一种是进程间通信使用的 System V IPC,另一种就是POSIX信号量:
#include <semaphore.h> //初始化信号量 int sem_init(sem_t *sem, int pshared,unsigned value); //销毁信号量 int sem_destroy(sem_t *sem); //以原子操作的方式将信号量的值+1 int sem_post(sem_t *sem); //以原子操作的方式将信号量的值-1 int sem_wait(sem_t *sem); //与sem_wait相同,不过始终立即返回,如果未成功返回-1并将errno置为EAGAIN int sem_trywait(sem_t *sem); //关闭命名信号量 int sem_close(sem_t *sem); //命名一个信号量 sem_t *sem_open(const char *name, int flag);
信号量的使用:(重点)
1.建立两个线程,两个线程分别将自己的整形数i从1递增到100,并规定两个线程的整形不能超过5
#include <bits/stdc++.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h>//提供对POSIX操作系统API的访问权限的头文件 #include <windows.h> //using namespace std; const int INF=100; sem_t sem_1,sem_2;//两个信号量 using std::cout; using std::endl; /* 思路:因为最大差值不能超过5,先初始化两个信号量为5,如果一个数+1,在wait自己sem的 同时post另一个sem,表示你可以多加一点,因为差值可以从[-5,5],所以必须post另一个sem才满足条件 */ void* run1(void *arg){ int i;//变量 for(i=0;i<=INF;i++){ sem_wait(&sem_1);//原子操作-1 usleep(500*1000); cout<<"the thread 1 print "<<i<<endl; sem_post(&sem_2);//原子操作+1 } pthread_exit(0); } void* run2(void *arg){ int i; for(i=0;i<=INF;i++){ sem_wait(&sem_2); usleep(500*1000); cout<<"the thread 2 print "<<i<<endl; sem_post(&sem_1); } pthread_exit(0); } signed main(){ sem_init(&sem_1,0,5); sem_init(&sem_2,0,5); pthread_t tid1=5000,tid2=10000; pthread_create(&tid1,NULL,run1,NULL); pthread_create(&tid2,NULL,run2,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); sem_destroy(&sem_1); sem_destroy(&sem_2); return 0; }2.经典线程问题:生产者消费者问题
#include <bits/stdc++.h> #include <unistd.h> #include <pthread.h> #include <windows.h> #include <semaphore.h> const int maxn=101; struct Stack{ int a[maxn]; int top; void init(){ memset(a,0,sizeof(a)); top=0; } void push(int num){ a[top++]=num; } int pop(){ return a[top--]; } }Q; sem_t Full,Empty; void* push(void *arg){ while(true){ sem_wait(&Full); int num=rand()%100000; printf("producter has produce an integer %d,now the top is %d\n",num,Q.top); Q.push(num); usleep(500*1000); sem_post(&Empty); } pthread_exit(0); } void *pop(void *arg){ while(true){ sem_wait(&Empty); printf("comsumer has comsume an integer %d,now the top is %d\n",Q.pop(),Q.top); usleep(500*1000); sem_post(&Full); } pthread_exit(0); } signed main(){ Q.init(); sem_init(&Full,0,100); sem_init(&Empty,0,0); pthread_t tid1,tid2; pthread_create(&tid1,NULL,push,NULL); pthread_create(&tid2,NULL,pop,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); sem_destroy(&Full); sem_destroy(&Empty); return 0; } -
互斥锁
互斥锁可以用于保护关键代码段,这有点向二进制的POSIX信号量
互斥锁具有以下特点:
`
·原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。``
·唯一性:如果一个线程锁定一个互斥量,在它接触锁定之前,没有其他线程可以锁定这个互斥量。``
·非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量`互斥锁的使用:
-
在访问共享资源后临界区域前,对互斥锁进行加锁;
-
在访问完成后释放互斥锁导上的锁。在访问完成后释放互斥锁导上的锁;
-
对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。
-
```c++
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
```
还有一个常用的方法就是:将互斥锁封装成对象,构成自动锁,这样生命周期结束的时候锁自动销毁
```c++
class CAutoMutex
{
public:
CAutoMutex()
{
mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
}
~CAutoMutex()
{
pthread_mutex_unlock(&mutex);
}
private:
pthread_mutex_t mutex;
};
```
死锁产生:对已经加锁的普通锁再次加锁,会构成死锁;两个对象按照不同的顺序申请互斥锁
```
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <windows.h>
#include <bits/stdc++.h>
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;
int a,b;
const int INF=100;
void* run(void *arg){
//锁b
int x=0;
while(x<=INF){
pthread_mutex_lock(&mutex_b);
printf("the chile thread got mutex b,waiting fo mutex a\n");
usleep(500*1000);
b++;
pthread_mutex_lock(&mutex_a);
a+=b++;
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
}
pthread_exit(NULL);
}
signed main(){
pthread_mutex_init(&mutex_a,NULL);
pthread_mutex_init(&mutex_b,NULL);
pthread_t tid;
pthread_create(&tid,NULL,run,NULL);
//向a锁加锁
int x=0;
while(x<=INF){
pthread_mutex_lock(&mutex_a);
printf("the father thread got mutex a,waiting for mutext b\n");
a++;
pthread_mutex_lock(&mutex_b);
b+=a++;
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
}
pthread_join(tid,NULL);
pthread_mutex_destroy(&mutex_a);
pthread_mutex_destroy(&mutex_b);
return 0;
}
```
-
条件变量
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直 到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步 的一种机制,主要包括两个动作:
-
一个线程等待"条件变量的条件成立"而挂起;
-
另一个线程使 “条件成立”(给出条件成立信号)。
更加清晰的说法就是,条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的 时候,唤醒等到这个共享数据的线程。
#include <pthread.h> // 初始化条件变量 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); // 阻塞等待 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); // 超时等待 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex, const timespec *abstime); // 解除所有线程的阻塞 int pthread_cond_destroy(pthread_cond_t *cond); // 至少唤醒一个等待该条件的线程 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒等待该条件的所有线程 int pthread_cond_broadcast(pthread_cond_t *cond);
-
-
本文深入探讨了C++在Linux环境下多线程编程的精髓,包括NPTL库的优势、线程模型、线程创建与管理、POSIX信号量、互斥锁和条件变量的使用,以及如何避免死锁,是理解和掌握多线程编程不可或缺的指南。

542

被折叠的 条评论
为什么被折叠?



