目录
Linux下线程创建函数pthread_ create()
线程的等待函数pthread_ join()
线程终止
函数pthread exit()
函数pthread_cancel()
分离线程pthread_detach()
线程间的互斥
线程间同步
死锁
进程和线程
线程和进程是一对有意义的概念,主要区别和联系如下:
- 进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了CPU资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。
- 线程是进程的一部分,如果没有进行显式地线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可以认为系统是多线程的。
- 多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。
- 进程有进程控制表PCB,系统通过PCB对进程进行调度;线程有线程控制表TCB.但是,TCB所表示的状态比PCB要少得多。
Linux下线程创建函数pthread_ create()
函数pthread_create()用于创建一个线程。
在pthread_create()函数调用时,传入的参数有线程属性、线程函数、线程函数变量,用于生成一个某种特性的线程, 线程中执行线程函数。创建线程使用函数pthread_ create(),它的原型为:
#include <pthread.h>
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),void *arg );
- thread:用于标识一个线程,它是一个pthread_ t类型的变量,在头文件pthreadtypes.h中定义:typedef unsigned long int pthread t;
- attr:这个参数用于设置线程的属性,设置为空,采用了默认属性。
- start_ routine:当线程的资源分配成功后,线程中所运行的单元,一般设置为自己编写的一个函数start_routine()。
- arg: 线程函数运行时传入的参数,将一个run的参数传入用于控制线程的结束。
当创建线程成功时,函数返回0;若不为0,则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。错误代码EAGAIN表示系统中的线程数量达到了上限,错误代码EINVAL表示线程的属性非法。
线程创建成功后,新创建的线程按照参数3和参数4确定一一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *threadRun(void *args)
{
while (true)
{
sleep(1);
cout << "我是子线程..." << endl;
}
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, threadRun, nullptr);
while (true)
{
sleep(1);
cout << "我是主线程..." << endl;
}
}
在编译时,我们需要链接线程库libpthread
g++ -o myphread myphread .cc -lpthread
从运行结果可以看到结果并不规律,主要是两个线程争夺CPU资源造成的。
线程的等待函数pthread_ join()
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
函数pthread_join()用来等待一 个线程运行结束。这个函数是阻塞函数,一直等到被等待的线程结束为止,函数才返回并且收回被等待线程的资源。函数原型为:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
- thread:线程的标识符,即pthread_create()函数创建成功的值。
- retval:线程返回值,它是一个指针,可以用来存储被等待线程的返回值。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:
1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
函数pthread exit()
void pthread_exit(void *retval);
线程终止函数,跟进程一样,无返回值,线程结束的时候无法返回到它的调用者(自身)。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
函数pthread_cancel()
取消一个执行中的线程
int pthread_cancel(pthread_t thread);
返回值:成功返回0;失败返回错误码 。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static int retvalue;//线程返回值
void *threadRun1(void *args)
{
cout << "我是子线程1..." << endl;
retvalue = 1;
return (void *)&retvalue;
}
void *threadRun2(void *args)
{
cout << "我是子线程2..." << endl;
retvalue = 2;
pthread_exit((void *)&retvalue);
}
void *threadRun3(void *arg)
{
while (1)
{
cout << "我是子线程3..." << endl;
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
void *ret;
// threadRun1 return
pthread_create(&tid, NULL, threadRun1, NULL);
pthread_join(tid, &ret);
cout << "子线程1返回... 返回值:" << *(int *)ret << endl;
// threadRun2 exit
pthread_create(&tid, NULL, threadRun2, NULL);
pthread_join(tid, &ret);
cout << "子线程2返回... 返回值:" << *(int *)ret << endl;
// threadRun3 cancel by other
pthread_create(&tid, NULL, threadRun3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
cout << "子线程3返回... 返回值:PTHREAD_CANCELED" << endl;
else
cout << "子线程3返回... 返回值:NULL" << endl;
}
分离线程pthread_detach()
int pthread_detach(pthread_t thread);
线程的分离状态决定线程的终止方法。线程的分离状态有分离线程和非分离线程两种。
在上面的例子中,线程建立的时候没有设置属性,默认终止方法为非分离状态。在这种情况下,需要等待创建线程的结束。只有当pthread_join()函数返回时,线程才算终止,并且释放线程创建的时候系统分配的资源。
分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。线程的分离方式可以根据需要,选择适当的分离状态。
当将一个线程设置为分离线程时,如果线程的运行非常快,可能在pthread_create()函数返回之前就终止了。由于一个线程在终止以后可以将线程号和系统资源移交给其他的线程使用,此时再使用函数pthread_create(获得 的线程号进行操作会发生错误。
线程间的互斥
进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
互斥锁是用来保护一段临界区的,它可以保证某时间段内只有一个线程在执行一段代码或者访问某个资源。下面一段代码是一个生产者/消费者的实例程序,生产者生产数据,消费者消耗数据,它们共用一个变量, 每次只有一个线程访问此公共变量。
线程互斥的函数介绍
与线程互斥有关的函数原型和初始化的常量如下,主要包含互斥的初始化方式宏定义、互斥的初始化函数pthread_mutx_init()、 互斥的锁定函数pthread_mutex_lock()、 互斥的预锁定函数pthread_mutx_trylock()、互斥的解锁函数pthread_mutx_unlock()、 互斥的销毁函数pthread_mutex_destroy()。
函数pthread_mutx_init(),初始化一 个 mutex变量,结构pthread_mutex_ t为系统内部私有的数据类型,在使用时直接用pthread_mutex_t就可以了,因为系统可能对其实现进行修改。属性为NULL,表明使用默认属性。
pthread_ mutex_lock()函数声明开始用互斥锁上锁,此后的代码直至调用pthread_mutx_unlock()函数为止,均不能执行被保护区域的代码,也就是说,在同时间内只能有一个线程执行。当一个线程执行到pthread_mutex _lock()函数处时,如果该锁此时被另一个线程使用,此线程被阻塞,即程序将等待另一个线程释放此互斥锁。互斥锁使用完毕后记得要释放资源,调用pthread_mutex_destroy()函数进行释放。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int buffer_has_item=0;//缓冲区计数值
pthread_mutex_t mutex;//互斥区
int running = 1;//线程运行控制
//生产者线程程序
void *producter_f (void *arg)
{
while (running)//没有设置退出值
{
pthread_mutex_lock(&mutex);//进入互斥区
buffer_has_item++;//增加计数值
printf("生产,总数量:%d\n",buffer_has_item); //打印信息
pthread_mutex_unlock (&mutex);//离开互斥区
}
}
//消费者线程程序
void *consumer_f (void *arg)
{
while (running)//没有设置退出值
{
pthread_mutex_lock(&mutex);//进入互斥区
buffer_has_item--;//减少计数值
printf("消费,总数量:%d\n",buffer_has_item);
pthread_mutex_unlock (&mutex);
}
}
int main ()
{
pthread_t consumer_t;//消费者线程参数
pthread_t producter_t;//生产者线程参数
pthread_mutex_init(&mutex , NULL);//初始化互斥
pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
usleep(1) ;//等待,线程创建完毕
running =0;//设置线程退出值
pthread_join(consumer_t, NULL);//等待消费者线程退出
pthread_join (producter_t, NULL) ;//等待生产者线程退出
pthread_mutex_destroy(&mutex);//销毁互斥
return 0;
}
线程间同步
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步。POSIX信号量是用于同步操作,达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。
线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量大于0的时候,才能访问信号量所代表的公共资源。
信号量的主要函数有信号量初始化函数sem_init()、 信号量的销毁函数sem_destroy()、
信号量的增加函数sem_pom()、信号量的减少函数sem_wait()等。 还有一个函数sem_trywait(),它的含义与互斥的函数pthread_mutex_trylock()是一致的,先对资源是否可用进行判断。函数的原型在头文件文件semaphore.h中定义。
1.线程信号量初始化函数sem_int()
sem_int()函数用来初始化一个信号量。它的原型为:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem指向信号量结构的一个指针, 当信号量初始化完成的时候,可以使用这个指针进行信号量的增加减少操作;
- 参数pshared用于表示信号量的共享类型,不为0时这个信号量可以在进程间共享,否则这个信号量只能在在当前进程的多个线程之间共享;
- 参数value用于设置信号量初始化的时候信号量的值。
2.线程信号量增加函数sem_post()
sem_post(函数的作用是增加信号量的值,每次增加的值为1。当有线程等待这个信号量的时候,等待的线程将返回。函数的原型为:
#include <semaphore.h>
int sem_post (sem_t *sem) ;
3.线程信号量等待函数sem_wait()
sem_wait()函数的作用是减少信号量的值,如果信号量的值为0,则线程会一直阻塞到信号量的值大于0为止。sem_wait()函数每次使信号量的值减少1,当信号量的值为0时不再减少。函数原型为:
#include <semaphore. h>
int sem_wait (sem_t *sem) ;
4.线程信号量销毁函数sem_destroy()
sem_destroy()函 数用来释放信号量sem,函数原型为:
#include <semaphore.h>
int sem_destroy(sem_t *sem) ;
5.线程信号量的例子
下面来看一个使用信号量的例子。在mutex的例子中,使用了一个全局变量来计数,在这个例子中,使用信号量来做相同的工作,其中一个线程增加信号量来模仿生产者,另一个线程获得信号量来模仿消费者。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
int running = 1;//线程运行控制
int cnt = 5;
//生产者
void *producter_f (void *arg)
{
int semval = 0;
while (running)
{
usleep(1);
sem_post(&sem);//信号量增加
sem_getvalue(&sem,&semval);//获取信号量的值
printf("生产,总数量:%d\n",semval);
}
}
//消费者
void *consumer_f (void *arg)
{
int semval = 0;
while (running)
{
usleep(1);
sem_wait(&sem);//信号量增加
sem_getvalue(&sem,&semval);//获取信号量的值
printf("消费,总数量:%d\n",semval);
}
}
int main ()
{
pthread_t consumer_t;//消费者线程参数
pthread_t producter_t;//生产者线程参数
sem_init(&sem , 0, 16);//信号量初始化
pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
usleep(1) ;//等待
running =0;//设置线程退出值
pthread_join(consumer_t, NULL);//等待消费者线程退出
pthread_join (producter_t, NULL) ;//等待生产者线程退出
sem_destroy(&sem);//信号量销毁
return 0;
}
从执行结果可以看出,各个线程间存在竞争关系。而数值并未按产生一个消耗一个的顺序显示出来,而是以交叉的方式进行,有的时候产生多个后再消耗多个,造成这种现象的原因是信号量的产生和消耗线程对CPU竞争的结果。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配