一、什么是线程?
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
二、什么时候使用多线程?
当多个任务可以并行执行时,可以为每个任务启动一个线程。
三、线程的基本函数
大多数pthread_XXX系列的函数在失败时,并未遵循UNIX函数的惯例返回-1,而是返回错误代码;如果调用成功,则返回0。
1)线程创建
#includeint pthread_create (pthread_t *__restrict __newthread,//指向pthread_create的指针, 用于引用新创建的线程 __const pthread_attr_t *__restrict __attr,//用于设置线程的属性,可以简单地设置为NULL void *(*__start_routine) (void *),//新创建的线程执行的函数 void *__restrict __arg)//执行函数的参数
2)线程终止
#includevoid pthread_exit(void *retval);//返回指针,指向线程要返回的某个对象
线程通过调用pthread_exit函数终止执行,并返回一个指向某对象的指针。注意:绝不能用它返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不存在了,这将引起严重的程序漏洞。
用pthread_exit只会使主线程自身退出,产生的子线程继续执行;用return则所有线程退出。
综合以上要想让子线程总能完整执行(不会中途退出),方法如下:
(1) 在主线程中调用pthread_join对其等待,即pthread_create / pthread_join / pthread_exit或return
(2) 一种方法是在主线程退出时使用pthread_exit,这样子线程能继续执行,即pthread_create / pthread_detach / pthread_exit
(3) 还有一种是pthread_create / pthread_detach / return,这时就要保证主线程不能退出,至少是子线程完成前不能退出。
3)线程同步
#includeint pthread_join(pthread_t th, void **thread_return);
参数说明:
th:线程ID,标识唯一线程。
thread_return:用户定义的指针,用来存储被等待线程的返回值。
4)线程分离
#includeint pthread_detach(pthread_t tid);
linux线程执行和windows不同,pthread有两种状态joinable状态和unjoinable状态。
如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符。只有当你调用了pthread_join之后这些资源才会被释放。
若线程是unjoinable状态,这些资源在线程函数退出时或pthread_exit时自动会被释放。unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:
pthread_detach(pthread_self());
将状态改为unjoinable状态,确保资源的释放。或者将线程置为 joinable,然后适时调用pthread_join。
其实简单的说就是在线程函数头加上 pthread_detach(pthread_self())的话,线程状态改变,在函数尾部直接 pthread_exit线程就会自动退出。
5)线程取消
1、 线程取消的函数
#includeint pthread_cancel(pthread_t thread);
2、设置线程的取消状态
#includeint pthread_setcancelstate(int state, int *oldstate);
参数说明:
state:可以是PTHREAD_CANCEL_ENABLE允许线程接收取消请求,也可以是PTHREAD_CANCEL_DISABLE忽略取消请求。 oldstate:获取先前的取消状态。如果对它没兴趣,可以简单地设置为NULL。如果取消请求被接受了,线程可以进入第二个控制层次,用pthread_setcanceltype设置取消类型。3、设置线程的取消类型
#includeint pthread_setcanceltype(int type, int *oldtype);
参数说明:
type:可以取PTHREAD_CANCEL_ASYNCHRONOUS,它将使得在接收到取消请求后立即采取行动;另一个是PTHREAD_CANCEL_DEFERRED,它将使得在接收到取消请求后,一直等待直到线程执行了下述函数之一后才采取行动:pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancel、sem_wait或sigwait。 oldtype:允许保存先前的状态,如果不想知道先前的状态,可以传递NULL。 默认情况下,线程在启动时的取消状态为PTHREAD_CANCEL_ENABLE,取消类型是PTHREAD_CANCEL_DEFERRED。下面代码是主线程向它创建的线程发送一个取消请求。
#include#include #include void *thread_function(void *arg); int main() { int res; pthread_t a_thread; void *thread_result; res = pthread_create(&a_thread, NULL, thread_function, NULL); if (res != 0) { perror("Thread create failed!"); exit(EXIT_FAILURE); } sleep(4); printf("Canceling thread.../n"); res = pthread_cancel(a_thread); if (res != 0) { perror("Thread cancel failed!"); exit(EXIT_FAILURE); } printf ("Waiting for thread to finished.../n"); res = pthread_join(a_thread, &thread_result); if (res != 0) { perror ("Thread join failed!"); exit(EXIT_FAILURE); } printf("Thread canceled!"); exit(EXIT_FAILURE); } void *thread_function(void *arg) { int i; int res; res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); if (res != 0) { perror("Thread setcancelstate failed!"); exit(EXIT_FAILURE); } res = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); if (res != 0) { perror("Thread setcanceltype failed!"); exit(EXIT_FAILURE); } printf("thread_function is running.../n"); for (i = 0; i < 10; i++) { printf("Thread is still running (%d).../n", i); sleep(1); } pthread_exit(0); }
运行结果:
thread_function is running...Thread is still running (0)...Thread is still running (1)...Thread is still running (2)...Thread is still running (3)...Canceling thread...Waiting for thread to finished...
5)线程的属性
下面介绍几个常用的函数:
1、初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 说 明:Posix线程中的线程属性主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。 pthread_attr_init实现时为属性对象分配了动态内存空间。 头文件:#include*/
2、销毁一个线程属性对象
int pthread_attr_destroy(pthread_attr_t *attr);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 说 明:经pthread_attr_destroy后的属性,再被pthread_create函数调用,将会返回错误。 头文件:#include*/
3、获取线程分离状态属性
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 detachstate 保存返回的分离状态属性 说 明:获取线程分离状态属性 头文件:#include*/
4、修改线程分离状态属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 detachstat 有两个取值 PTHREAD_CREATE_DETACHED(分离) PTHREAD_CREATE_JOINABLE(非分离) 说 明:修改线程分离状态属性 头文件:#include*/
5、获取线程的作用域
int pthread_attr_getscope(pthread_attr_t *attr, int *scope);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 scope 返回线程的作用域 说 明:指定了作用域也就指定了线程与谁竞争资源 头文件:#include*/
6、设置线程的作用域
int pthread_attr_setscope(pthread_attr_t *attr, int scope);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 guardsize 线程的作用域,可以取如下值 PTHREAD_SCOPE_SYSTEM 与系统中所有进程中线程竞争 PTHREAD_SCOPE_PROCESS 与当前进程中的其他线程竞争 说 明:指定了作用域也就指定了线程与谁竞争资源 头文件:#include*/
7、获取线程的堆栈信息(栈地址和栈大小)
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 stackaddr 返回获取的栈地址 stacksize 返回获取的栈大小 说 明:获取线程的堆栈地址和大小 头文件:#include*/
8、设置线程堆栈区
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 stackaddr 线程的堆栈地址:应该是可移植的,对齐页边距的 可以用posix_memalign来进行获取 stacksize 线程的堆栈大小:应该是页大小的整数倍 说 明:设置堆栈区,将导致pthread_attr_setguardsize失效。 头文件:#include*/
9、获取线程堆栈地址
int pthread_attr_getstackaddr(pthread_attr_t *attr, void **stackaddr);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 stackaddr 返回获取的栈地址 说 明:函数已过时,一般用pthread_attr_getstack来代替 头文件:#include*/
10、设置线程堆栈地址
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 stackaddr 设置线程堆栈地址 说 明:函数已过时,一般用pthread_attr_setstack来代替 头文件:#include*/
11、获取线程堆栈大小
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 stacksize 返回线程的堆栈大小 说 明:获取线程堆栈大小 头文件:#include*/
12、设置线程堆栈大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 guardsize 线程的栈保护区大小:应该是页大小的整数倍 说 明:设置线程堆栈大小: 头文件:#include*/
13、获取线程的调度策略
int pthread_attr_getschedpolicy(pthread_attr_t *attr, int *policy);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 policy 返回线程的调度策略 说 明:获取线程的调度策略 头文件:#include*/
14、设置线程的调度策略
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 policy 线程的调度策略,有如下三种: SCHED_FIFO 先入先出策略 SCHED_RR 轮转调度,类似于 FIFO,但加上了时间轮片算法 SCHED_OTHER 系统默认策略 说 明:设置线程的调度策略 头文件:#include*/
15、获取线程的调度参数
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 param 返回获取的调度参数,该结构仅有一个从参数,如下 struct sched_param { int sched_priority; /* Scheduling priority */ }; 说 明:获取线程的调度参数 头文件:#include*/
16、设置线程的调度参数
int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *param);/* 返回值:若是成功返回0,否则返回错误的编号 形 参: attr 指向一个线程属性的指针 param 要设置的调度参数 说 明:设置线程的调度参数 头文件:#include*/
6)线程的同步
1、用信号量进行同步
(1) 信号量创建
#includeint sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
sem:信号量对象。 pshared:控制信号量的类型,0表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。 value:信号量的初始值。(2) 信号量控制
#includeint sem_wait(sem_t *sem);int sem_post(sem_t *sem);
sem_post的作用是以原子操作的方式给信号量的值加1。
sem_wait的作用是以原子操作的方式给信号量的值减1,但它会等到信号量非0时才会开始减法操作。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有线程增加了该信号量的值使其不再为0。(3) 信号量销毁
#includeint sem_destory(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误。
与大多数Linux函数一样,这些函数在成功时都返回0。下面编码实现输入字符串,统计每行的字符个数,以“end”结束输入:
#include#include #include #include #include #define SIZE 1024 void *thread_function(void *arg); char buffer[SIZE]; sem_t sem; int main() { int res; pthread_t a_thread; void *thread_result; res = sem_init(&sem, 0, 0); if (res != 0) { perror("Sem init failed"); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); if (res != 0) { perror("Thread create failed"); exit(EXIT_FAILURE); } printf("Input some text. Enter 'end' to finish/n"); while (scanf("%s", buffer)) { sem_post(&sem); if (strncmp("end", buffer, 3) == 0) break; } printf ("/nWaiting for thread to finish.../n"); res = pthread_join(a_thread, &thread_result); if (res != 0) { perror("Thread join failed"); exit(EXIT_FAILURE); } printf ("Thread join/n"); sem_destroy(&sem); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sem_wait(&sem); while (strncmp("end", buffer, 3) != 0) { printf("You input %d characters/n", strlen(buffer)); sem_wait(&sem); } pthread_exit(NULL); }
运行结果:
Input some text. Enter 'end' to finish123You input 3 characters1234You input 4 characters12345You input 5 charactersend Waiting for thread to finish…Thread join
2、用互斥量进行线程同步
它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。
#includeint pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t, *mutexattr);int pthread_mutex_lock(pthread_mutex_t *mutex);//pthread_mutex_trylock()与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);int pthread_mutex_destory(pthread_mutex_t *mutex);
与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,所以必须对函数的返回代码进行检查。互斥量的属性设置这里不讨论,因此设置成NULL。
互斥锁的属性在创建锁的时候指定,在Linux Threads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
* PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
* PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
* PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
* PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
我们用互斥量来重写刚才的代码如下:
#include#include #include #include #include #define SIZE 1024 char buffer[SIZE]; void *thread_function(void *arg); pthread_mutex_t mutex; int main() { int res; pthread_t a_thread; void *thread_result; res = pthread_mutex_init(&mutex, NULL); if (res != 0) { perror("Mutex init failed!"); exit(EXIT_FAILURE); } res = pthread_create(&a_thread, NULL, thread_function, NULL); if (res != 0) { perror("Thread create failed!"); exit(EXIT_FAILURE); } printf("Input some text. Enter 'end' to finish/n"); while (1) { pthread_mutex_lock(&mutex); scanf("%s", buffer); pthread_mutex_unlock(&mutex); if (strncmp("end", buffer, 3) == 0) break; sleep(1); } res = pthread_join(a_thread, &thread_result); if (res != 0) { perror("Thread join failed!"); exit(EXIT_FAILURE); } printf("Thread joined/n"); pthread_mutex_destroy(&mutex); exit(EXIT_SUCCESS); } void *thread_function(void *arg) { sleep(1); while (1) { pthread_mutex_lock(&mutex); printf("You input %d characters/n", strlen(buffer)); pthread_mutex_unlock(&mutex); if (strncmp("end", buffer, 3) == 0) break; sleep(1); } }
运行结果:
Input some text. Enter 'end' to finish123You input 3 characters1234You input 4 characters12345You input 5 charactersendYou input 3 charactersThread joined
3、用条件变量进行同步
条件变量条件变量用pthread_cond_t数据类型表示。条件变量本身由互斥量保护,所以在改变条件状态前必须锁住互斥量。
(1) 条件变量初始化:
第一种,赋值常量PTHREAD_COND_INITIALIZER;
第二种,使用pthread_cond_init函数:
int pthread_cond_init (pthread_cond_t *__restrict __cond, __const pthread_condattr_t *__restrict __cond_attr);int pthread_cond_destroy (pthread_cond_t *__cond);
(2) 条件等待:
使用pthread_cond_wait等待条件为真。pthread_cond_wait (pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex)
这里需要注意的是,调用pthread_cond_wait传递的互斥量已锁定,pthread_cond_wait将调用线程放入等待条件的线程列表,然后释放互斥量,在pthread_cond_wait返回时,再次锁定互斥量。
(3) 唤醒线程
pthread_cond_signal唤醒等待该条件的某个线程,pthread_cond_broadcast唤醒等待该条件的所有线程。int pthread_cond_signal (pthread_cond_t *__cond);int pthread_cond_broadcast (pthread_cond_t *__cond)
示例如下:
#include#include #include #include #include #define DEBUG 1int num=0;pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER;pthread_cond_t qready=PTHREAD_COND_INITIALIZER;void * thread_func(void *arg){ int i=(int)arg; int ret; sleep(5-i);//线程睡眠,然最先生成的线程,最后苏醒 pthread_mutex_lock(&mylock);//调用pthread_cond_wait前,必须获得互斥锁 printf("thread %d is running \n",i); num++; pthread_mutex_unlock(&mylock);//解锁 pthread_cond_broadcast(&qready);//唤醒等待该条件的所有线程 return (void *)0;}int main(int argc, char** argv) { int i=0,err; pthread_t tid[4]; void *tret; for(;i<4;i++) { err=pthread_create(&tid[i],NULL,thread_func,(void *)i); if(err!=0) { printf("thread_create error:%s\n",strerror(err)); exit(-1); } } for (i = 0; i < 4; i++) { err = pthread_join(tid[i], &tret); if (err != 0) { printf("can not join with thread %d:%s\n", i,strerror(err)); exit(-1); } } return 0;}
4、用读写锁进行同步
读写锁也称为共享-独占(shared-exclusive)锁,允许多个线程同时读,只要没有写模式下的加锁,任意线程都可以进行读模式下的加锁;只能有一个线程同时写,只有读写锁处于不加锁状态时,才能进行写模式下的加锁。读写锁非常适合读数据的频率远大于写数据的频率从的应用中。到目前为止读写锁仍然不是属于POSIX标准。
(1) 读写锁初始化:
#includeint pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock, __const pthread_rwlockattr_t *__restrict __attr);int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);
(2) 读/写加锁:
int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock)int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock)
(3) 读/写解锁:
int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);
5、信号量、互斥锁、条件变量的区别
(1) 信号量和互斥锁的区别
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的。也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
(2) 何时互斥锁不够,还需要条件变量?
假设有共享的资源sum,与之相关联的mutex 是lock_s。假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++。那么只用mutex足够了。程序员只要确保每个线程操作前,取得lock,然后sum++,再unlock即可。每个线程的代码将像这样:
add(){ pthread_mutex_lock(lock_s); sum++; pthread_mutex_unlock(lock_s);}
如果操作比较复杂,假设线程t0,t1,t2的操作是sum++,而线程t3则是在sum到达100的时候,打印出一条信息,并对sum清零。这种情况下,如果只用mutex,则t3需要一个循环,每个循环里先取得lock_s,然后检查sum的状态。如果sum>=100,则打印并清零,然后unlock。如果sum >= 100,则unlock,并sleep()本线程合适的一段时间。
这个时候,t0,t1,t2的代码不变,t3的代码如下:
print(){ while (1) { pthread_mutex_lock(lock_s); if(sum<100) { printf(“sum reach 100!”); pthread_mutex_unlock(lock_s); } else { pthread_mutex_unlock(lock_s); my_thread_sleep(100); return OK; } }}
这种办法有以下问题:
首先,sum在大多数情况下不会到达100,那么对t3的代码来说,大多数情况下,走的是else分支,只是lock和unlock,然后sleep(),这浪费了CPU处理时间。 其次,为了节省CPU处理时间,t3会在探测到sum没到达100的时候sleep()一段时间。这样却又带来另外一个问题,亦即t3响应速度下降。可能在sum到达200的时候,t4才会醒过来。 这样。程序员在设置sleep()时间的时候陷入两难境地,设置得太短了节省不了资源,太长了又降低响应速度.真是难办啊!这个时候。condition variable内裤外穿,从天而降,拯救了焦头烂额的你。你首先定义一个condition variable:
pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER;
t0,t1,t2的代码只要后面加两行,像这样:
add(){ pthread_mutex_lock(lock_s); sum++; pthread_mutex_unlock(lock_s); if(sum>=100) pthread_cond_signal(&cond_sum_ready);}
而t3的代码则是:
print{ pthread_mutex_lock(lock_s); while(sum<100) pthread_cond_wait(&cond_sum_ready, &lock_s); printf(“sum is over 100!”); sum=0; pthread_mutex_unlock(lock_s); return OK;}
注意两点:
1) 在thread_cond_wait()之前,必须先lock相关联的mutex, 因为假如目标条件未满足,pthread_cond_wait()实际上会unlock该mutex, 然后block,在目标条件满足后再重新lock该mutex, 然后返回。
2) 为什么是while(sum<100),而不是if(sum<100) ?这是因为在pthread_cond_signal()和pthread_cond_wait()返回之间,有时间差,假设在这个时间差内,还有另外一个线程t4又把sum减少到100以下了,那么t3在pthread_cond_wait()返回之后,显然应该再检查一遍sum的大小。这就是用 while的用意。