目录
线程
认识线程
什么是线程
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程也就是一个轻量级进程,每个线程都有自己的线程控制块,即一个进程至少有一个轻量级进程,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在线程组里面,所有的线程都是对等的关系,没有父线程的概念。
线程与进程关系
- 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
- 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
- 进程可以蜕变成线程
- 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
线程间资源共享情况
共享资源:
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间
非共享资源:
- 线程id
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量
- 信号屏蔽字
- 调度优先级
线程优缺点
优点:①提高程序的并发性;②开销小,不用重新分配内存;③通信和共享数据方便。
缺点:①线程不稳定(库函数实现);②线程调试比较困难(gdb支持不好);③线程无法使用unix经典事件,例如信号;
线程原语
pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
/*参数释义:
pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
const pthread_attr_t *attr:线程属性设置,如使用默认属性,则传NULL
void *(*start_routine) (void *):函数指针,指向新线程应该加载执行的函数模块
void *arg:指定线程将要加载调用的那个函数的参数
返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。*/
//注(1):创建线程时,没什么特殊情况我们都是使用默认属性的,不过有时候需要做一些特殊处理,碧如调整优先级啊这些的。
pthread_self
获取调用线程tid
#include<pthread.h>
pthread_t pthread_self(void);
代码示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
printids(arg);
return NULL;
}
int main()
{
int err;
pthread_t ntid;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
if (err != 0)
{
fprintf(stderr, "can't create thread: %s\n", strerror(err));
exit(1);
}
printids("main thread:");
sleep(1);
return 0;
}
结果:
new thread: pid 4721 tid 3087018896 (0xb8002b90)
main thread: pid 4721 tid 3087021760 (0xb80036c0)
pthread_exit
调用线程退出函数,注意和exit函数的区别,任何线程里exit导致进程退出,其他线程
未工作结束,主控线程退出时不能return或exit。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是
用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函
数已经退出了
#include <pthread.h>
void pthread_exit(void *retval);
参数释义
retval:线程退出时传递出的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。
pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数释义:
thread:回收线程的tid
retval:接收退出线程传递出的返回值
返回值:成功返回0,失败返回错误号
函数阻塞调用线程直到thread所指定的线程终止。
如果在目标线程中调用pthread_exit(),程序员可以在主线程中获得目标线程的终止状态。
连接线程只能用pthread_join()连接一次。若多次调用就会发生逻辑错误。
说了这么多为什么要使用pthread_join()?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。
可以这么简单的理解:主线程等待子线程的终止。也就是想调用pthread_join()方法后面的代码,只有等到子线程结束了才能继续执行。
pthread_cancel
在进程内某个线程可以取消另一个线程
#include <pthread.h>
int pthread_cancel(pthread_t thread);
代码实例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return (void *)1;
}
void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
while(1)
{
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code %d\n", (int)tret);
return 0;
}
结果:
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1
pthread_detach
分离线程
#include <pthread.h>
int pthread_detach(pthread_t tid);
参数释义
tid:分离线程tid
返回值:成功返回0,失败返回错误号。
注意:一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
代码实例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *thr_fn(void *arg)
{
int n = 3;
while (n--)
{
printf("thread count %d\n", n);
sleep(1);
}
return (void *)1;
}
int main(void)
{
pthread_t tid;
void *tret;
int err;
pthread_create(&tid, NULL, thr_fn, NULL);
//第一次运行时注释掉下面这行,第二次再打开,分析两次结果
pthread_detach(tid);
while (1)
{
err = pthread_join(tid, &tret);
if (err != 0)
fprintf(stderr, "thread %s\n", strerror(err));
else
fprintf(stderr, "thread exit code %d\n", (int)tret);
sleep(1);
}
return 0;
}
1、注释:pthread_detach(tid);
结果:
thread count 2
thread count 1
thread count 0
thread exit code 1
thread No such process
thread No such process
2、不注释:pthread_detach(tid);
结果:
thread count 2
thread Invalid argument
thread count 1
thread Invalid argument
thread count 0
thread Invalid argument
thread Invalid argument
pthread_equal
比较两个线程是否相等
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
线程终止方式
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
①从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
②一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
③线程可以调用pthread_exit终止自己。
线程同步
线程为什么要同步
- 共享资源,多个线程都可对共享资源操作
- 线程操作共享资源的先后顺序不确定
- 处理器对存储器的操作一般不是原子操作
互斥量
临界区(Critical Section)
保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区选定
临界区的选定因尽可能小,如果选定太大会影响程序的并行处理性能。
mutex操作原语
pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER //用于初始化互斥锁,后面简称锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化锁,和上面那个一个意思。
//初始化一个互斥锁(互斥量)–>初值可看做1
int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
int pthread_mutex_unlok(pthread_mutex_t *mutex); //解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁
扩展:
- restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
- 静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL);
- attr对象用于设置互斥量对象的属性,使用时必须声明为pthread_mutextattr_t类型,默认值可以是NULL。Pthreads标准定义了三种可选的互斥量属性:
①协议(Protocol): 指定了协议用于阻止互斥量的优先级改变
② 优先级上限(Prioceiling):指定互斥量的优先级上限
③进程共享(Process-shared):指定进程共享互斥量
互斥锁有什么作用?
采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个进程在进入临界区时应得到锁;它在退出临界区时释放锁。
互斥量实例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5
int counter; /* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, doit, NULL);
pthread_create(&tidB, NULL, doit, NULL);
/* wait for both threads to terminate */
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
return 0;
}
void *doit(void *vptr)
{
int i, val;
for (i = 0; i < NLOOP; i++)
{
pthread_mutex_lock(&counter_mutex);
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
counter = val + 1;
pthread_mutex_unlock(&counter_mutex);
}
return NULL;
}
//从运行结果不难看出,线程tidA首先进行循环打印,等到tidA完成释放资源后才轮到线程tidB
运行结果:
b7f80b90: 1
b7f80b90: 2
b7f80b90: 3
b7f80b90: 4
b7f80b90: 5
b757fb90: 6
b757fb90: 7
b757fb90: 8
b757fb90: 9
b757fb90: 10
死锁
死锁问题是多线程特有的问题,它可以被认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中应该坚决避免和杜绝的问题。
线程死锁的原因
(1)互斥条件:一个资源每次只能被一个线程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决方法:如果发生了死锁,那么只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。
条件变量
- 条件变量提供了另一种同步的方式。互斥量通过控制对数据的访问实现了同步,而条件变量允许根据实际的数据值来实现同步。
- 没有条件变量,程序员就必须使用线程去轮询(可能在临界区),查看条件是否满足。这样比较消耗资源,因为线程连续繁忙工作。条件变量是一种可以实现这种轮询的方式。
- 条件变量往往和互斥一起使用
条件变量控制原语
//初始化条件变量:
//方法一:静态初始化
pthread_cont_t cont = PTHREAD_COND_INITIALIZER;
//方法二:动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//参数释义:cond:用于接收初始化成功管道条件变量
//attr:通常为NULL,且被忽略
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
//等待
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); //计时等待
//等待唤醒
int pthread_cond_signal(pthread_cond_t *cptr); //唤醒一个等待该条件的线程。存在多个线程是按照其队列入队顺序唤醒其中一个
int pthread_cond_broadcast(pthread_cond_t * cptr); //广播,唤醒所有等待线程
实例:
生产者消费者模型:
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg
{
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for(;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
pthread_cond_wait(&has_product, &lock);
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
for (;;)
{
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
运行结果:
Produce 543
Consume 543
Produce 465
Consume 465
······