目录
进程概念
线程概念
线程进程基本操作
一、创建线程
二、线程属性
三、线程终止
四、线程安全
五、其他操作
进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。
在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的基本单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。
线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。
引入线程的好处
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。。
(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
在Linux系统中,多线程的管理使用 pthread_t
功能 | 进程 | 线程 |
---|---|---|
创建 | fork() | pthread_create() |
退出 | exit | pthread_exit() |
等待 | wait/waitpid() | pthread_join() |
取消 | abort() | pthread_cancel() |
获取ID | getpid() | pthread_self() |
调度策略 | SCHED_OTHER、SCHED_FIFO、SCHED_RR | SCHED_OTHER、SCHED_FIFO、SCHED_RR |
通信机制 | 管道、消息队列、共享内存、信号、信号量 | 信号、信号量、互斥锁、读写锁、条件变量 |
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
第一个参数为指向线程标识符的指针,也就是线程对象的指针
第二个参数用来设置线程属性。
第三个参数是线程运行函数的地址,通俗理解线程要执行函数(线程做的事情的)指针。一般这个函数执行时间比较长(有while循环),做的事情比较多。如果单次动作(执行时间比较短),也就无需多线程执行了。
最后一个参数是线程要运行函数的参数。
线程的默认堆栈大小是1MB,就是说,系统每创建一个线程就要至少提供1MB的内存,那么,创建线程失败,极有可能就是内存不够用了。
pthread_create会导致内存泄露! pthread_create创建的线程结束后,系统并未回收其资源,从而导致了泄露。
为什么要分离线程?
线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。
从上面的描述中可以得知如果调用pthread_create函数创建一个默认非分离状态的线程,如果不用pthread_join()函数,线程结束时并不算终止,所以仍然会占用系统资源。这里有如下几种方法解决这个问题:
如果新线程创建后,不用pthread_join()等待回收新线程,那么就会造成内存泄漏,但是当等待新线程时,主线程就会一直阻塞,影响主线程处理其他链接要求,这时候就需要一种办法让新线程退出后,自己释放所有资源,因此产生了线程分离。
1.使用pthread_join()函数回收相关内存区域。
int pthread_detach(pthread_t thread);将已经运行中的线程设定为分离状态;
pthread_t tid;
void* state;
pthread_create(&tid, NULL, test, NULL);
pthread_join(tid, &state);
pthread_join使一个线程等待另一个线程结束。代码中如果没有pthread_join 主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。pthread_join()函数以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。
2.可以调用 pthread_detach() 函数分离线程。
pthread_t tid;
pthread_create(&tid, NULL, test, NULL);
pthread_detach(tid);
当然,也可以在 thread function 中调用。
void* test(void* arg)
{
.....
pthread_detach(pthread_self());
return NULL;
}
3.使用线程属性 pthread_attr_setdetachstate
pthread_attr_t attr;
pthread_t tid;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, test, NULL);
sleep(3);//等待线程结束
pthread_attr_destroy(&attr);
pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项:
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void * stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
int pthread_attr_setdetachstate (pthread_attr_t *__attr, int __detachstate)设定线程的分离状态
int pthread_attr_getdetachstate (const pthread_attr_t *__attr,int *__detachstate)获取线程参数中的分离状态;
int pthread_attr_setschedpolicy (pthread_attr_t *__attr, int __policy),设定线程的调度策略;
int pthread_attr_getschedparam (const pthread_attr_t *__restrict __attr, struct sched_param *__restrict __param)
//获取参数中的线程优先级;
int pthread_attr_setschedparam (pthread_attr_t *__restrict __attr, const struct sched_param *__restrict __param)
//设定线程的优先级;
int pthread_attr_setinheritsched (pthread_attr_t *__attr, int __inherit)
//设定线程调度策略的继承属性,该函数必须在root权限下调用;
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
//设定线程优先级的可竞争范围:
pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。
int pthread_attr_init (pthread_attr_t *__attr), 初始化pthread创建参数;
为了设置这些属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、pthread_attr_destroy()和与各个属性相关的pthread_attr_get---/pthread_attr_set---函数。
void pthread_exit(void *value_ptr); //在线程执行的函数中调用此接口
例子:
void *thread_run(void* arg)
{
while(1)
{
printf("new thread,thread is :%u,pid:%d\n",pthread_self(),getpid());
sleep(1);
pthread_exit(NULL);
}
}
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
例子:
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,NULL);
while(1)
{
printf("main thread,thread is :%u,pid:%d\n",pthread_self(),getpid()) ;
sleep(3);
pthread_cancel(pthread_self());//杀死自己,pthread_self()是获取主线程id
}
return 0;
}
int pthread_setcancelstate(int state, int *oldstate)
int pthread_setcanceltype(int type, int *oldtype)
多个线程同时操作临界资源而不会出现数据二义性,实现线程安全:
多线程的同步与互斥的区别
假如把整条道路看成是一个【进程】的话,那么马路中间白色虚线分隔开来的各个车道就是进程中的各个【线程】了
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。
1.互斥
互斥如何实现:
互斥锁: 一个1/0的计数器
1标识完成加锁,加锁就是计数-1;
操作完毕后要解锁, 解锁就是计数+1
0表示不可以加锁, 不能加锁则等待
互斥锁的操作步骤:
1.定义互斥锁变量 pthread_mutex_t
pthread_mutex_t lock;
2.初始化互斥锁变量 pthread_mutex_init
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
3.加锁 pthread_mutex_lock
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞式加锁
int pthread_mutex_lock(pthread_mutex_t* mutex)//阻塞式加锁
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
4.解锁 pthread_mutex_unlock
int pthread_mutex_unlock(pthread_mutex_t* mutex)//解锁
5.销毁互斥锁 pthread_mutex_destory
int pthread_mutex_destroy(pthread_mutex_t* mutex);
死锁::对所资源的竞争以及进程/线程加锁的推进顺序不当,因为对一些无法加锁的锁进行加锁而导致程序卡死
死锁产生的四个必要条件:
避免死锁:破坏必要条件
死锁处理:死锁检测算法 ,银行家算法
2.同步
条件变量:描述某些资源就绪与否的状态,为了实现同步而引入。同步是以互斥为前提的。少数情况可实现无锁同步。
1.定义条件变量 pthread_cond_t
pthread_cond_t condition;
2.初始化条件变量 pthread_cond_init
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数: pthread_cond_attr是用来设置pthread_cond_t的属性,当传入的值是NULL的时候表示使用默认的属性。
3.等待 / 唤醒 pthread_cond_wait / pthread_cond_signal
int pthread_cond_broadcast(pthread_cond_t* cond);//唤醒条件变量等待的所有线程
int pthread_cond_signal(pthread_cond_t* cond)//唤醒条件变量上等待的一个线程
int pthread_cond_wait(pthread_cond_t* cond)//解锁互斥量并等待条件变量触发
int pthread_cond_timewait(pthread_cond_t* cond,int abstime)//pthread_cond_wait,但可设定等待超时
4.销毁条件变量 pthread_cond_destroy
int pthread_cond_destroy(pthread_cond_t* cond);
条件变量为什么要搭配互斥锁使用?
因为条件变量本身只提供等待与唤醒的功能,具体要什么时候等待需要用户来进行判断.这个条件的判断,通常涉及临界资源的操作(其他线程要通过修改条件,来促使条件满足), 而这个临界资源的操作应该受到保护.因此要搭配互斥锁一起使用。
3.互斥和同步的联合使用
pthread_mutex_lock(&mutex)
while or if(线程执行的条件是否成立)
pthread_cond_wait(&cond,&mutex);
线程执行
pthread_mutex_unlock(&mutex);
进程id:
这里所说的进程ID指我们通过fork创建子进程,子进程和父进程在内核中独立运行,并且一个进程对应一个进程描述符(PCB),PCB中包含了进程的ID,通过getpid返回当前进程ID
线程id:
#include
pid_t tid;
tid = syscall(SYS_gettid); //在线程执行的函数中调用此接口
#include
pthread_t pthread_self(void); //在线程执行的函数中调用此接口
返回值:成功返回0,失败返回错误码
注:这里的ID是一个地址,而不是向上面两个ID是一个整数
对于单线程的进程,内核中tid==pid,对于多线程进程,他们有相同的pid,不同的tid。tid用于描述内核真实的pid和tid信息。
pthread_self返回的是posix定义的线程ID,man手册明确说明了和内核线程tid不同。它只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id。
描述线程的id,为什么需要两个不同的ID呢?
答:这是因为线程库实际上由两部分组成:内核的线程支持+用户态的库支持(glibc),Linux在早期内核不支持线程的时候glibc就在库中(用户态)以纤程(就是用户态线程)的方式支持多线程了,POSIX thread只要求了用户编程的调用接口对内核接口没有要求。linux上的线程实现就是在内核支持的基础上以POSIX thread的方式对外封装了接口,所以才会有两个ID的问题。
gettid 获取的是内核中真实线程ID, 对于多线程进程来说,每个tid实际是不一样的。
而pthread_self获取的是相对于进程的线程控制块的首地址, 只是用来描述统一进程中的不同线程