线程
前言
0.问题引入
前面讲到,为了并发执行任务(程序),现代操作系统引入“进程”的概念
分析: (1)创建一个进程的开销比较大,why? 子进程会拷贝(“写时复制”)父进程整个地址空间
进程间的地址空间是独立
(2)进程间通信,需要用到第三方(如:内核), copy copy p1------->内核--------->p2
于是,就有人提出能不能在同一个进程内部实现"任务(程序)"的并发执行?
线程/轻量级进程
1.线程 Thread
线程是比进程更小的活动单位,它是进程中的一个执行路径(执行分支), 线程也是并发一种情形。 进程内部可以有多个线程,他们并发执行,但进程内部所有的线程 共享整个进程的地址空间.
main函数 是进程的一个主线程 进程的指令部分分成多个 线程去并发执行
线程特点: (1)创建一个线程比创建一个进程开销要小得多,why? 因为创建一个线程,不需要拷贝进程得地址空间。
(2)实现线程得通信十分方便,why? 因为一个进程内部所有得线程直接共享整个进程的地址空间。
(3)线程也是一个动态概念 进程(线程)状态: ready running blocking
(4)自从有了线程后, 系统调度的单位是以线程为单位; 资源分配的单位是进程 ---------------------------------------------------------------------------- 线程是进程内部的一个指令的执行分支,多个线程,就是多个指令序列并发执行。 指令必须在函数内部,线程的指令部分肯定会封装在一个函数内部。 这个函数,我们称之为“线程函数”:一个线程创建后,要执行的指令全部在该 函数内。这个线程函数执行完毕,线程的任务也就结束啦、
线程函数的原型: void *(*start_routine) (void *); 线程函数: 带一个void *的指针, 返回一个void *指针。 如: void *my_thread(void *data) { } -------------------------- thread的实现有多种,比较常用的是POSIX 线程 pthread
2.linux中pthread的接口函数
(1)创建一个线程:pthread_create 线程有一个线程id,类似于进程id(pid),用来 唯一标识一个线程的。在pthread中,用类型pthread_t来描述一个 线程id的。
线程属性: 线程栈空间大小 stack:用来存放局部变量的。 线程优先级 … 在pthread中,线程属性用结构体 pthread_attr_t来描述。 同时提供了几个用于改变线程属性的函数接口,不建议程序员 直接修改pthread_attr_t的结构体。“默认属性” 头文件 #include <pthread.h> 函数功能 用来创建一个子线程 函数原型 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
函数参数 pthread_t *thread //指向的空间,用来保存新创建的线程的id的; const pthread_attr_t *attr //指定线程的属性的,一般为NULL,表示采用默认属性 void *(*start_routine) (void *) //指向线程对应的线程函数。线程的任务就是去 执行start_routine指向的函数. void *arg //将作为线程函数的实参传入
返回值 成功:返回0 失败:返回-1,同时errno被设置 Compile and link with -pthread.
(2)线程退出 (2.1)线程函数返回 (2.2)在线程执行的任意时刻 调用 pthread_exit 头文件 #include <pthread.h> 函数功能 让指定的子线程退出 函数原型 void pthread_exit(void *retval); 函数参数 void *retval //保存子线程的返回值(退出值) 你所指定的一个值所在的地址 返回值 无 Compile and link with -pthread.
(2.3)It is cancelled 线程被别人“取消”(其他线程调用 pthread_cancel) t1:pthread_cancel(t2) t1 调用函数pthread_cancel取消(干掉)t2这个线程 是不是t2就一定会被干掉呢?不一定 这个得依赖t2这个线程得一个线程属性; 它是否可以被cancelled. 这个“可被取消属性”pthread提供了一个函数接口去修改它: 头文件 #include <pthread.h> 函数功能 设置子线程得属性 函数原型 int pthread_setcancelstate(int state, int *oldstate); 函数参数 int state //要设置的”取消属性”的状态 PTHREAD_CANCEL_ENABLE:该线程可被取消 PTHREAD_CANCEL_DISABLE:该线程不可被取消 int *oldstate //保存上一次”取消属性“的状态 返回值 成功:返回0 失败:返回其他值
头文件 #include <pthread.h> 函数功能 取消一个子线程 函数原型 int pthread_cancel(pthread_t thread); 函数参数 pthread_t thread //接收“取消请求”的线程的id 返回值 成功:返回0 失败:返回非0
一个线程退出了,并不代表它所有的资源都被释放了。 一个线程退出了,它的资源是否完全被释放,取决于它一个属性 detach 分离属性: ENABLE:分离 该线程结束,它所有资源会自动释放。 DISABLE:非分离 《----默认属性 该线程结束,它会有部分资源不会自动释放 非得要其他线程调用pthread_join这个函数才能 完全释放
头文件 #include <pthread.h> 函数功能 用来设置一个线程的属性为“分离状态” 函数原型 int pthread_detach(pthread_t thread); 函数参数 pthread_t thread //设置线程属性的那个线程id 返回值 成功:返回0 失败:返回非0 Compile and link with -pthread.
如: pthread_detach(pthread_self());
函数原型 pthread_t pthread_self(void) //这个函数用来获取调用者线程的id
返回值 一定成功:返回调用者线程的id
(3)等待一个线程退出 pthread_join:用来等待(并释放)一个指定的线程退出的。
头文件 #include <pthread.h> 函数功能 用来等待(并释放)一个指定的线程退出的。 函数原型 int pthread_join(pthread_t thread, void **retval); 函数参数 pthread_t thread //要等待退出的那个线程的id void **retval //二级指针,用来保存退出线程的返回值的。 返回值 成功:返回0 失败:返回其他值
//线程函数
void *routine(void *)
{
void *ret;
....
return ret
}
//void *p; //用来保存routine函数的返回值
pthread_join(, &p);
pthread_join(, retval = &p)
{
*(&p) <==> p
*retval = ret;
}
(4)线程间的同步/互斥机制 为了线程间,有序地访问共享资源,也要引入“信号量机制”。 (4.1)信号量(System V semaphore /POSIX semaphore)
(4.2)线程互斥锁 线程互斥锁也是一个信号量,只不过线程互斥锁,存在于 进程的地址空间,在用于线程间同步于互斥时候,线程互斥锁 效率要比信号量高。
需要man查看线程互斥锁的相关API,就得安装manpages-posix-dev手册 sudo apt-get install manpages-posix-dev
pthread_mutex_t:来描述一个线程互斥锁 (1)初始化线程互斥锁 pthread_mutex_init
头文件 #include <pthread.h> 函数功能 初始化线程互斥锁 函数原型 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 函数参数 restrict //C语言中的一种类型限定符,用于告诉编译器,对象已经被指针所引用 不能通过除了该指针外所有其他直接或简接的方式修改该对象的内容。 编译器能更高效率的进行一些处理而不用担心影响到别的指针。
pthread_mutex_t *restrict mutex //待初始化的线程互斥锁的指针
const pthread_mutexattr_t *restrict attr //线程互斥锁的属性,一般为NULL,
采用默认属性。
如:线程互斥锁默认初始化值为1,Unlock
返回值 成功:返回0 失败:返回非0
(2)线程互斥锁 P操作(Lock上锁) pthread_mutex_lock“死等” pthread_mutex_trylock“非阻塞等” pthread_mutex_timedlock“限时等待”
"死等"如果锁不可用,则一直阻塞(wait)直到锁可用或 被信号打断(出错了) 头文件 #include <pthread.h> 函数功能 用来获取线程互斥锁(上锁) 函数原型 int pthread_mutex_lock(pthread_mutex_t *mutex) 函数参数 pthread_mutex_t *mutex //要获取的线程互斥锁的指针 返回值 成功:返回0,表示获取了该互斥锁。-》可以进入临界区执行 失败:返回-1,表示出错了,没有获取到互斥锁。不可以进入临界区
非阻塞版本: 能获取则获取,不能获取立马返回,不等待 函数原型 int pthread_mutex_trylock(pthread_mutex_t *mutex); 返回值 成功:返回0,表示获取了该互斥锁 失败:返回其他值,表示没有获取到。 限时等待 头文件 #include <pthread.h> #include <time.h> 函数原型 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); 函数参数 const struct timespec *restrict abstime //绝对时间。 返回值 成功:返回0表示获取了该互斥锁 失败:返回其他值,表示没有获取该互斥锁
例子:
struct timespec tv;
clock_gettime(CLOCK_REALTIME,&tv); //用来获取系统的时间
tv.tv_sec = tv.tv_sec + 2;
int r = pthread_mutex_timedlock(mutex,&tv);
if(r != 0)
{
//表示没有获取
}
(3)线程互斥锁 V操作(Ulock,解锁) 函数原型 int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值 成功:返回0表示释放了 该互斥锁 失败:返回非0 表示没有释放该互斥锁
(4)线程互斥锁 销毁操作 函数原型 int pthread_mutex_destroy(pthread_mutex_t *mutex); 返回值 成功:返回0表示销毁该互斥锁 失败:返回非0 表示没有释放该互斥锁。
(5)"生产者-消费者"模型 (1)共享资源的互斥问题 信号量/线程互斥锁 (2)当缓冲区没有数据时,消费者线程,该怎么呢? a:不停地去测试,看有没有数据。 “轮询”,但是“轮询”会有天生缺陷: 浪费CPU 轮询会有一个时间差,不及时?
b:让出cpu(sleep),当有数据的时候,再唤醒我(wake up)
线程条件变量。 “同步”。
(6)线程条件变量是个什么东西? 在多线程程序设计中,我们可以用"条件变量"表示一个 特点的条件或事件 pthread_cond_t : 条件变量的类型
至于这个条件变量,到底表示什么条件或事件, 完全让程序员来解释或定义
在条件变量上有三种操作: 初始化 等待一个条件变量(等待该条件变量所代表的事件) 唤醒一个条件(唤醒正在等待该事件的线程)
线程1 线程2 … … if(条件不满足/事件没产生) { wait(条件变量/事件); } … 条件/事件产生了 wakeup(条件变量上等待的线程)
…消费那个事件啦
例子: “生产者-消费者”
int data = 0
main 主线程 --》生产者 data++;
t1线程 --》消费者 when data >= 100000000 data = 0
----具体的线程条件变量的API函数接口:
(1)初始化一个条件变量 头文件 #include <pthread.h> 函数功能 初始化一个条件变量 函数原型 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 函数参数 pthread_cond_t *restrict cond //要初始化的那个条件变量 const pthread_condattr_t *restrict attr //条件变量的属性,一般为NULL 表示采用默认属性 返回值 成功:返回0 失败:返回其他值 (2)等待一个条件变量:等待条件变量所代表的那个事件, 头文件 #include <pthread.h> 函数功能 等待一个条件变量 函数原型 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 函数参数 pthread_cond_t *restrict cond //等待的那个条件变量 pthread_mutex_t *restrict mutex //线程互斥锁。为了保护cond所代表的事件/共享资源 返回值 成功:返回0(被其他线程唤醒了) 失败:返回其他值
注意: 在调用pthread_cond_wait时,mutex要是"locked"的一个状态。 这难道不会有问题吗?不会
pthread_cond_wait()
{
mutex Lock状态。
...
Unlock mutex:解锁
让出CPU。 //sleep状态,调用者线程就阻塞
...
when 条件产生时,其他的“生产者线程”唤醒你
lock mutex:锁住。
}
//限时等待
函数原型 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
返回值 成功:返回0(被其他线程唤醒了) 失败:返回其他值。 (3)唤醒一个条件变量:唤醒正在等待条件变量所描述的那个事件的线程 头文件 #include <pthread.h> 函数功能 唤醒一个条件变量 函数原型 int pthread_cond_broadcast(pthread_cond_t *cond); //“广播”:唤醒所有等待线程 int pthread_cond_signal(pthread_cond_t *cond); //只唤醒一个线程 函数参数 pthread_cond_t *cond //要唤醒的那个条件变量 返回值 成功:返回0 失败:返回其他值
(4)销毁一个条件变量 头文件 #include <pthread.h> 函数功能 销毁一个条件变量 函数原型 int pthread_cond_destroy(pthread_cond_t *cond); 函数参数 pthread_cond_t *cond //要销毁的那个条件变量 返回值 成功:返回0 失败:返回其他值 作业: 1.用线程的方式实现一个目录(只考虑目录和普通文件的情况)的拷贝
void cp_dir(char *src, char *dest)
{
opendir(src)
while(readdir())
{
//子目录
cp_dir(src1, dest1);
//普通文件
创建一个线程,让线程执行cp的那个拷贝函数
}
}
void cp(char *file1, char *file2)
{
while()
{
read file1
write file2
}
}
线程总结 1.进程于线程的区别?
2.线程并发相比于进程,有什么优势?
3.线程互斥锁的手段 信号量 线程互斥锁
4.生产者-消费者模型 共享资源的互斥问题 同步问题
思考问题: 1.linux并发可以用进程,也可以线程 用进程并发与线程并发区别? 何时用进程?何时用线程?
2.什么是可重入?可重入函数? 线程安全是什么?
|