一. Linux 多线程概述
1、概述
进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段和堆栈段。故当在进行上下文切换时,开销较大,于是出现了线程。线程通常叫做轻量级进程,线程是是进程的基本调度单元,每个进程至少都有一个 main 线程。它与同进程中的其他线程共享进程空间{堆 代码 数据 文件描述符 信号等},只拥有自己的栈空间,大大减少了上下文切换的开销。
线程和进程在使用上各有优缺点:线程执行开销小,占用的 CPU 少,线程之间的切换快,但不利于资源的管理和保护;而进程正相反。从可移植性来讲,多进程的可移植性要好些。要注意的是,由于线程共享了进程的资源和地
址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。
2、线程分类
按调度者分为用户级线程和核心级线程
- 用户级线程:主要解决上下文切换问题,调度算法和调度过程全部由用户决定,在运行时不需要特定的内核支持。缺点是无法发挥多处理器的优势。
- 核心级线程:允许不同进程中的线程按照同一相对优先调度方法调度, 发挥多处理器的并发优势。
3、Linux下的线程
Linux 的线程是通过用户级的函数库实现的,一般采用 pthread 线程库实现线程的访问和控制。它用第3方 posix 标准的 pthread,具有良好的可移植性。编译的时候要在后面加上 –lpthread 。
创建 退出 等待
多进程 fork() exit() wait()
多线程 pthread_create pthread_exit() pthread_join()
注意的是,在使用线程函数时,不能随意使用exit退出函数进行出错处理, 由于 exit 的作用是使调用进程终止,往往一个进程包括了多个线程,所以在线程中通常使用 pthread_exit 函数来代替进程中的退出函数 exit。
函数原型:
#include <pthread.h>
int pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);
void pthread_exit(void *retval);
通常的形式为:
pthread_t pthid;
pthread_create(&pthid,NULL,pthfunc,NULL);或
pthread_create(&pthid,NULL,pthfunc,(void*)3);
pthread_exit(NULL);或 pthread_exit((void*)3);
//3 作为返回值被 pthread_join 函数捕获。
函数pthread_create用来创建线程。返回值:成功,则返回0;失败,则返回对应错误码。
各参数描述如下:
- 参数 thread 是传出参数,保存新线程的标识;
- 参数 attr 是一个结构体指针,结构中的元素分别指定新线程的运行属性,attr可以用 pthread_attr_init 等函数设置各成员的值,但通常传入为 NULL 即可;
- 参数 start_routine 是一个函数指针,指向新线程的入口点函数, 线程入口点函数带有一个 void *的参数由 pthread_create 的第 4 个参数传入;
- 参数 arg 用于传递给第 3 个参数指向的入口点函数的参数, 可以为 NULL,表示不传递。
函数 pthread_exit 表示线程的退出。 其参数可以被其它线程用 pthread_join 函数捕获。
eg:
#include <stdio.h>
#include <pthread.h>
void *ThreadFunc(void *pArg) //参数的值为 123
{
int i = 0;
for(; i<10; i++)
{
printf("Hi,I'm child thread,arg is:%d\n", (int)pArg);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t thdId;
pthread_create(&thdId, NULL, ThreadFunc, (void *)123 );
int i = 0;
for(; i<10; i++)
{
printf("Hi,I'm main thread,child thread id is:%x\n", thdId);
sleep(1);
}
return 0;
}
编译时需要带上线程库选项: gcc -o a a.c -lpthread
3. 线程的等待退出
线程从入口点函数自然返回,或者主动调用 pthread_exit()函数,都可以让线程正常终止线程从入口点函数自然返回时,函数返回值可以被其它线程用 pthread_join 函数获取。
pthread_join 原型为:
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
1.该函数是一个阻塞函数, 一直等到参数 th 指定的线程返回; 与多进程中的 wait 或 waitpid 类似。thread_return 是一个传出参数, 接收线程函数的返回值。 如果线程通过调用 pthread_exit()终止, 则
pthread_exit()中的参数相当于自然返回值, 照样可以被其它线程用 pthread_join 获取到。
2.thid 传递 0 值时,join 返回 ESRCH 错误。
3. 该函数还有一个非常重要的作用, 由于一个进程中的多个线程共享数据段, 因此通常在一个线程退出后,退出线程所占用的资源并不会随线程结束而释放。如果 th 线程类型并不是自动清理资源类型的,则 th 线程退出后,线程本身的资源必须通过其它线程调用 pthread_join 来清除,这相当于多进程程序中的 waitpid。
3、线程的取消(cancel)
线程取消的方法是一个线程向目标线程发 cancel 信号, 但是如何处理 cancel 信号则由目标线程自己决定,目标线程或者忽略、或者立即终止、或者继续运行至 cancelation-point(取消点)后终止。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
取消点:
根据 POSIX 标准, pthread_join()、 pthread_testcancel()、 pthread_cond_wait()、pthread_cond_timedwait()、 sem_wait()、 sigwait()等函数以及 read()、 write()等会引起阻塞的系统调用都是 Cancelation-point, 而其他 pthread 函数都不会引起 Cancelation 动作。
4、线程的终止清理函数
线程为了访问临界共享资源而为其加上锁, 但在访问过程中该线程被外界取消, 或者发生了中断, 则该临界资源将永远处于锁定状态得不到释放。 外界取消操作是不可预见的, 因此的确需要一个机制来简化用于资源释放的编程。
在 POSIX 线程 API 中提供了一个 pthread_cleanup_push()/pthread_cleanup_pop()函数对用于自动释放资源--从 pthread_cleanup_push()的调用点到 pthread_cleanup_pop()之间的程序段中的终止动作都将执行 pthread_cleanup_push()所指定的清理函数。
- void pthread_cleanup_push(void (*routine) (void *), void *arg)
- void pthread_cleanup_pop(int execute)
- pthread_cleanup_push()/pthread_cleanup_pop()采用先入后出的栈结构管理
pthread_cleanup_pop 的参数 execute
如果为非 0 值,则按栈的顺序注销掉一个原来注册的清理函数,并执行该函数; 当其为 0 时,仅仅在线程调用 pthread_exit 函数或者其它线程对本线程调用 pthread_cancel 函数时,才在弹出“清理函数”的同时执行该“清理函数”。
5、线程的互斥
因为多个线程共用进程的资源, 要访问的是公共区间时(全局变量)。故为了使其各个线程互斥访问临界资源,故需要加锁。在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。
创建互斥锁:
动态方式是采用 pthread_mutex_init()函数来初始化互斥锁, API 定义如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
其中 mutexattr 用于指定互斥锁属性(见下) , 如果为 NULL 则使用缺省属性。 通常为 NULL。
pthread_mutex_destroy()用于注销一个互斥锁,API 定义如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态
互斥锁属性结构体的定义为:
typedef struct
{
int __mutexkind; //注意这里是两个下划线
} pthread_mutexattr_t;
互斥锁的属性在创建锁的时候指定, 在 LinuxThreads 实现中仅有一个锁类型属性__mutexkind,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。有三个值可供选择:
- PTHREAD_MUTEX_TIMED_NP, 这是缺省值(直接写 NULL 就是表示这个缺省值),也就是普通锁(或快速锁)。 当一个线程加锁以后, 其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优
先级获得锁。这种锁策略保证了资源分配的公平性
-
- 示例: 初始化一个快速锁。
- pthread_mutex_t lock;
- pthread_mutex_init(&lock, NULL);
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。如果是不同线程请求, 则在加锁线程解锁时重新竞争。
-
- 示例: 初始化一个嵌套锁。
- pthread_mutex_t lock;
- pthread_mutexattr_t mutexattr;
- mutexattr.__mutexkind = PTHREAD_MUTEX_RECURSIVE_NP;
- pthread_mutex_init(&lock, &mutexattr);
- PTHREAD_MUTEX_ERRORCHECK_NP, 检错锁, 如果同一个线程请求同一个锁, 则返回EDEADLK,否则与 PTHREAD_MUTEX_TIMED_NP 类型动作相同。 这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。 如果锁的类型是快速锁, 一个线程加锁之后, 又加锁, 则此时就是死锁。
-
- 示例: 初始化一个嵌套锁。
- pthread_mutex_t lock;
- pthread_mutexattr_t mutexattr;
- mutexattr.__mutexkind = PTHREAD_MUTEX_ERRORCHECK_NP;
- pthread_mutex_init(&lock, &mutexattr);
锁操作:
加锁 int pthread_mutex_lock(pthread_mutex_t *mutex)
解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex)
pthread_mutex_lock:加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回 EPERM;在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁。
pthread_mutex_unlock:根据不同的锁类型, 实现不同的行为:
- 对于快速锁, pthread_mutex_unlock 解除锁定;
- 对于递规锁, pthread_mutex_unlock 使锁上的引用计数减 1;
- 对于检错锁, 如果锁是当前线程锁定的, 则解除锁定, 否则什么也不做。
注意:如果线程在加锁后解锁前被取消, 锁将永远保持锁定状态, 因此如果在关键区段内有取消点存在,则必须在退出回调函数pthread_cleanup_push/pthread_cleanup_pop 中解锁。同时不应该在信号处理函数中使用互斥锁, 否则容易造成死锁。
6、线程的同步
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作: 一个线程等待条件变量的条件成立而挂起; 另一个线程使条件成立(给出条件成立信号) 。为了防止竞争, 条件变量的使用总是和一个互斥锁结合在一起。
创建:
动态方式调用 pthread_cond_init()函数,API 定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
注:cond_attr 值通常为 NULL,且被忽略
注销一个条件变量需要调用 pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回 EBUSY。 因为 Linux 实现的条件变量没有分配什么资源, 所以注销动作只包括检查是否有等待线程。
API 定义如下:
int pthread_cond_destroy(pthread_cond_t *cond);
2、 等待和激发
等待条件有两种方式:无条件等待 pthread_cond_wait()和计时等待 pthread_cond_timedwait():
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);
注:通常做 pthread_cond_wait 之前, 往往要用pthread_mutex_lock 进行加锁, 而调用 pthread_cond_wait 函数会将锁解开,然后将线程挂起阻塞。 直到条件被 pthread_cond_signal 激发,再将锁状态恢复为锁定状态,最后再用 pthread_mutex_unlock 进行解锁)。
激发条件有两种形式, pthread_cond_signal()激活一个等待该条件的线程, 存在多个等待线程时按入队顺序激活其中一个; 而 pthread_cond_broadcast()则激活所有等待线程
eg:
#include"func.h"
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}cond,*pcon;
void *confAct(void *p)
{
printf("I am child pthread ,create successful\n");
pcon p1=(pcon)p;
pthread_mutex_lock(&p1->mutex);
pthread_cond_wait(&p1->cond,&p1->mutex);
pthread_mutex_unlock(&p1->mutex);
puts("I am wake up");
pthread_exit(NULL);
}
int main()
{
cond t;
pthread_t pthid;
pthread_cond_init(&t.cond,NULL);
pthread_mutex_init(&t.mutex,NULL);
int ret;
ret=pthread_create(&pthid,NULL,confAct,&t);
sleep(1);
pthread_cond_signal(&t.cond);
pthread_join(pthid,NULL);
puts("I am main pthead!");
pthread_exit(NULL);
}