线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属于一个进程的其他的线程共享进程拥有的全部资源。
进程与线程
Linux进程创建一个新线程时,线程将拥有自己的栈(因为线程有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号句柄和当前目录状态。
Linux通过fork创建子进程与创建线程之间是有区别的:fork创建出该进程的一份拷贝,这个新进程拥有自己的变量和自己的PID,它的时间调度是独立的,它的执行几乎完全独立于父进程。
进程可以看成一个资源的基本单位,而线程是程序调度的基本单位,一个进程内部的线程之间共享进程获得的时间
线程的优点
1、和进程相比,它是一种非常”节俭”的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。
2、运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。
3、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。
除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:
1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。
线程操作
①创建
#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数说明:
thread:指向pthread_t类型的指针,用于引用新创建的线程。
attr:用于设置线程的属性,一般不需要特殊的属性,所以可以简单地设置为NULL。
(*start_routine)(void ):传递新线程所要执行的函数地址。
arg:新线程所要执行的函数的参数。
调用如果成功,则返回值是0,如果失败则返回错误代码。
与创建进程不同,创建线程时可以指定一个工作函数,新线程将从这个函数开始执行,函数返回也就等价于线程退出。
工作函数必须有一个(void *)型参数,新线程开始执行时,这个参数的值就是pthread_create函数的arg参数的值,因此可以利用它来向线程传递数据。
工作函数必须有(void *)型的返回值,它代表线程的退出状态。
②获取线程ID
include <pthread.h>
pthread_t pthread_self(void);
返回当前线程的ID
可以使用pthread_equal()函数检查两个线程ID是否相同,如果相等则返回0
include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
③线程终止
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
retval:返回指针,指向线程向要返回的某个对象。
线程通过调用pthread_exit函数终止执行,并返回一个指向某对象的指针。注意:绝不能用它返回一个指向局部变量的指针,因为线程调用结束后,这个局部变量就不存在了,这将引起严重的程序漏洞。
调用pthread_exit()等价于在工作函数中执行return,区别是pthread_exit()可以在工作函数调用的任何函数中被调用。
如果主线程调用pthread_exit(),而不是exit()或执行return,则其他线程将继续执行。
④线程等待
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
参数说明:
th:将要等待的线程,线程通过pthread_create返回的标识符来指定。
thread_return:一个指针,指向另一个指针,而后者指向线程的返回值。不需要返回值则为NULL
⑤线程分离
#include <pthread.h>
int pthread_detach(pthread_t thread);
成功返回0。失败返回错误值
一个线程可以使用下面的方式分离自己:
pthread_detach(pthread_self());
pthread_detach()不会导致调用者阻塞,也不会导致所操作的线程结束。如果调用pthread_detach()时线程已经结束,则清理其所占用的资源。
对于创建时处于未分离状态的线程,必须调用一次pthread_join()或pthread_detach(),否则线程结束后就会留下没有释放的资源。
注意事项
除了局部变量以外,所有其他变量都将在一个进程中的所有线程之间共享。
线程没有像进程那样的父子关系,仅有属于同一个进程的“同组”关系(进程实际上代表的是一个线程组)。
在POSIX线程模型中,主线程可以创建一个新线程A,新线程A又可以创建另一个新线程B,线程A和B本身没有父子关系,只是同属于一个进程。
等待线程结束的pthread_join()操作可以由任何一个同组的线程发起,不必是主线程。另外,如果主线程退出,即进程退出,则所有的线程也会随之退出。
线程的创建
#include <stdio.h>
#include <pthread.h>
void *thread_run(void* a)
{
while (1)
{
sleep(1);
printf ("线程 函数\n");
}
}
int main()
{
pthread_t thread_id;
int ret = pthread_create(&thread_id, NULL, thread_run, NULL);
if (ret != 0)
{
perror ("pthread_create");
return -1;
}
while (1)
{
sleep(1);
printf ("main 函数\n");
}
return 0;
}
线程的参数传递
#include <stdio.h>
#include <pthread.h>
struct data
{
int a;
int b;
};
void *pthread_run(void *a)
{
struct data *value = (struct data *)a;
printf("线程接收到参数: %d, %d\n",value->a, value->b);
int ret = value->a + value->b;
// 不能通过 pthread_exit 返回栈上变量的地址
pthread_exit( (void *)ret );
}
int main()
{
struct data value;
value.a = 10;
value.b = 20;
pthread_t id;
// 给线程传参要注意 栈上的变量的生命周期
int ret = pthread_create( &id, NULL, pthread_run, (void *)&value );
if( ret != 0 )
{
perror("pthread_create");
return -1;
}
sleep(1);
int result;
pthread_join(id, (void **)&result);
printf("result = %d\n",result);
return 0;
}
线程同步
当多个线程共享相同的内存时,需要确保每个线程看到一致的数据。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不存在一致性问题。同样地,如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。
线程同步方法:信号量、互斥量、条件变量
(1)信号量
信号量通常有两种:二进制信号量和计数信号量。二进制信号量只有0和1两种取值,计数信号量有更大的取值范围。
①创建
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个未命名的信号量(unnamed semaphore)。
sem指向需要初始化的信号量(sem_t类型)。
value指定信号量的初始值。
pshared表明信号量是在一个进程的多个线程之间共享还是在多个进程之间共享。若pshared为0,信号量被一个进程的多个线程共享,此时应该将信号量(sem_t)置于所有线程可见的位置(全局变量或动态分配)。
执行成功返回0,出错返回-1,并设置errno。
注意:初始化一个已经初始化了的信号量将导致未定义的行为。
②信号量控制
#include <semaphore.h>
int sem_post(sem_t *sem); // v
int sem_wait(sem_t *sem); // p
sem_post的作用是以原子操作的方式给信号量的值加1
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。例如,对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再为0为止。
如果两个线程同时在sem_wait函数上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
补充:还有另外一个信号量函数sem_trywait,它是sem_wait的非阻塞版本。
③信号量销毁
#include <semaphore.h>
int sem_destroy(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误。
④生产者消费者模型
多线程并发应用程序有一个经典的模型,即生产者/消费者模型。系统中,产生消息的是生产者,处理消息的是消费者,消费者和生产者通过一个缓冲区进行消息传递。生产者产生消息后提交到缓冲区,然后通知消费者可以从中取出消息进行处理。消费者处理完信息后,通知生产者可以继续提供消息。
要实现这个模型,关键在于消费者和生产者这两个线程进行同步。也就是说:只有缓冲区中有消息时,消费者才能够提取消息;只有消息已被处理,生产者才能产生消息提交到缓冲区。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <time.h>
#include <string.h>
// 信号量和缓冲区
struct data
{
sem_t empty; // 用来控制生产者,只有缓冲区为空,生产者才可以生产消息
sem_t full; // 用来控制消费者,只有缓冲区有数据,才可以消费
char buf[32]; // 消息缓冲区
};
struct data msg;
// 生产者线程工作函数
void *Produce(void *v)
{
char *buf[] = {"苹果", "梨", "香蕉", "榴莲", "橙子", "西瓜", "芒果", "火龙果"};
while (1)
{
// 只有当缓冲区空才能进,生产消息
sem_wait(&msg.empty);
strcpy(msg.buf, buf[rand()%8]);
printf ("放了一个水果: %s\n", msg.buf);
int time = rand() % 100 + 1;
usleep(time*10000);
// 生产完了,通知消费者进行消费
sem_post(&msg.full);
}
}
// 消费者线程工作函数
void *Consum(void *v)
{
char buf[32];
while (1)
{
// 只有当缓冲区不为空才能进,消费消息
sem_wait(&msg.full);
strcpy(buf, msg.buf);
printf ("吃了一个 %s\n", buf);
int time = rand() % 100 + 1;
usleep(time*10000);
// 消费完了,通知生产则会进行生产
sem_post(&msg.empty);
}
}
int main()
{
srand ((unsigned int)time(NULL));
// 初始化信号量
sem_init(&msg.empty, 0, 1); // 生产者,一开始要生产消息
sem_init(&msg.full, 0, 0); // 消费者,一开始要不能消费消息
pthread_t produceId;
pthread_t consumId;
// 创建生产者线程
pthread_create(&produceId, NULL, Produce, NULL);
// 创建消费者线程
pthread_create(&consumId, NULL, Consum, NULL);
// 等待线程结束
pthread_join(produceId, NULL);
pthread_join(consumId, NULL);
// 销毁信号量
sem_destroy(&msg.empty);
sem_destroy(&msg.full);
return 0;
}
(2)互斥量
互斥量(mutex)从概念上来说类似于一个二进制信号量,即初始值为1的信号量。互斥量被获取之后就不能再被获取,因此对互斥体的获取和释放操作常常称为加锁和解锁操作。
互斥量只能由获取它的线程进行释放,如果违反这一原则,则结果是未定义的。
互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥量。
①初始化
互斥量用一个pthread_mutex_t型的变量表示。使用互斥量之前需要对它进行初始化,其接口函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex参数指向要初始化的互斥量。attr参数指向一个描述互斥量属性的结构体。attr参数可以为NULL,表示使用默认属性。
②操作函数
互斥量的主要操作函数如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_lock()用于对mutex参数指向的互斥量进行加锁。如果这时互斥量已经被锁,则调用这个函数的线程将被阻塞,直到互斥量成为未锁状态。函数返回时,表示这个互斥量已经变为已锁状态,同时,函数的调用者成为这个互斥量的拥有者。
pthread_mutex_trylock()也用于对mutex参数指向的互斥量进行加锁。如果这时互斥量已经被锁,则函数以错误状态返回。
pthread_mutex_unlock()用于对mutex参数指向的互斥量进行解锁。如果这时互斥量是未锁状态或不是当前线程所拥有的,则结果未定义。 因此,互斥量必须在同一线程上成对出现。
③销毁
互斥量不用以后,应该使用下面的函数进行销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
④生产者消费者模型改进
如果将缓冲区设计为一个先进先出的队列,可以同时容纳多条消息,那么只要缓冲区不满,生产者就可以提交消息;同时,只要缓冲区不空,消费者就可以取出消息进行处理。这将大大提高整个程序的效率。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <time.h>
#include <string.h>
#include "SqQueue.h"
// 信号量和缓冲区
struct data
{
sem_t empty; // 用来控制生产者,只有缓冲区为空,生产者才可以生产消息
sem_t full; // 用来控制消费者,只有缓冲区有数据,才可以消费
Queue q; // 缓冲区队列
};
struct data msg;
// 互斥锁
pthread_mutex_t mutex;
int num = 0;
// 生产者线程工作函数
void *Produce(void *v)
{
while (1)
{
int time = rand() % 100 + 1;
usleep(time*10000);
// 只要队列不满 就能生产消息, empty代表当前队列剩余的空间
sem_wait(&msg.empty);
pthread_mutex_lock(&mutex); // 抢锁
num++; // 生产一个消息
// 将消息放入到队列里面
EnQueue (&(msg.q), num);
printf ("生产一条消息\n");
pthread_mutex_unlock(&mutex); // 解锁
// 生产完了,通知消费者进行消费
sem_post(&msg.full);
}
}
// 消费者线程工作函数
void *Consum(void *v)
{
char buf[32];
while (1)
{
int time = rand() % 100 + 1;
usleep(time*10000);
// 只有缓冲区有数据,就能消费消息, full当前队列消息的个数
sem_wait(&msg.full);
pthread_mutex_lock(&mutex); // 抢锁
int num;
DeQueue(&(msg.q), &num); // 去队列里取出一条消息
printf("消费了一条消息: %d\n", num);
pthread_mutex_unlock(&mutex); // 解锁
// 消费完了,通知生产则会进行生产
sem_post(&msg.empty);
}
}
int main()
{
srand ((unsigned int)time(NULL));
// 初始化信号量
sem_init(&msg.empty, 0, 10); // 生产者,一开始要生产 10 条消息
sem_init(&msg.full, 0, 0); // 消费者,一开始要不能消费消息
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 初始化队列
InitQueue(&(msg.q));
pthread_t produceId;
pthread_t consumId;
int i = 0;
for (i = 0; i < 5; i++)
{
// 创建生产者线程
pthread_create(&produceId, NULL, Produce, NULL);
pthread_detach(produceId);
}
// 创建消费者线程
pthread_create(&consumId, NULL, Consum, NULL);
// 等待线程结束
pthread_join(consumId, NULL);
// 销毁信号量
sem_destroy(&msg.empty);
sem_destroy(&msg.full);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
队列相关函数:
#ifndef __SQQUEUE_H__
#define __SQQUEUE_H__
#define TRUE 1
#define FALSE 0
#define SIZE 11
typedef int QueueData;
typedef struct _queue
{
QueueData data[SIZE];
int front; // 指向队头的下标
int rear; // 指向队尾的下标
}Queue;
// 置空队
int InitQueue (Queue *Q);
// 判队空否
int QueueEmpty (Queue *Q);
// 判队满否
int QueueFull (Queue *Q);
// 进队
int EnQueue (Queue *Q, QueueData x);
// 出队
int DeQueue (Queue *Q, QueueData *x);
// 取队头
int GetFront (Queue *Q, QueueData *x);
#endif // __SQQUEUE_H__
#include "SqQueue.h"
int InitQueue (Queue *q)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
// 置空队
q->front = 0;
q->rear = 0;
return TRUE;
}
int QueueEmpty (Queue *q)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
return q->front == q->rear;
}
int QueueFull (Queue *q)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
return q->front == (q->rear+1)%SIZE;
}
int EnQueue (Queue *q, QueueData x)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
if (QueueFull(q))
{
errno = FULL_QUEUE;
return FALSE;
}
q->rear = (q->rear+1) % SIZE;
q->data[q->rear] = x;
return TRUE;
}
int DeQueue (Queue *q, QueueData *x)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
if (QueueEmpty(q))
{
errno = EMPTY_QUEUE;
return FALSE;
}
q->front = (q->front + 1) % SIZE;
*x = q->data[q->front];
return TRUE;
}
int GetFront (Queue *q, QueueData *x)
{
if (q == NULL)
{
errno = ERROR;
return FALSE;
}
if (QueueEmpty(q))
{
errno = EMPTY_QUEUE;
return FALSE;
}
int index = (q->front + 1) % SIZE;
*x = q->data[index];
return TRUE;
}
(3)条件变量
假设有这样一种情况:线程正在等待共享数据内某个条件出现,这时必须先解锁,否则其他线程不可能更改共享数据。一种实现方式是,可以循环检测共享数据,但是在检测前要加锁,检测后又要解锁,这样效率会很低。
因此,在这种情况下,需要一种方法,使得当线程在等待某些条件的满足时进入睡眠状态,一旦条件满足,线程就应该被唤醒继续执行。这个方法就是使用POSIX条件变量。
①初始化
POSIX条件变量由一个pthread_cond_t型的变量表示。使用条件变量前需要对它进行初始化,所用的接口函数原型如下:
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
cond参数指向要初始化的条件变量,attr参数指向描述条件变量属性的结构体。attr可以为NULL,表示使用默认属性。
②等待
当程序中需要等待一个条件变量时,可以用下面的函数:
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这些函数都有一个指向互斥量的参数mutex,说明条件变量必须与互斥量搭配使用。 调用这些函数时,首先,指定的互斥量将被释放,然后线程将阻塞,等待条件变量的触发。函数返回前,互斥量重新被线程获取。
pthread_cond_timedwait与pthread_cond_wait的区别在于,前者有一个由abstime指定的超时时间限制,当线程阻塞的时间超过了这个时间就会自动醒来。
③初始化
触发一个条件变量可以用以下函数:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal可以唤醒一个或多个正在等待cond参数所指向的条件变量的线程,而pthread_cond_broadcast则可以唤醒全部正在等待cond参数所指向的条件变量的线程。
注意:POSIX标准只规定pthread_cond_signal函数唤醒至少一个睡眠中的线程,并没有规定只唤醒一个。
④销毁
条件变量不用之后,应该用下面的函数进行销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
生产者消费者改进
#include <stdio.h>
#include <pthread.h>
#include <time.h>
#include <string.h>
// 互斥锁
pthread_mutex_t mutex;
// 条件变量
pthread_cond_t cond;
// 共享资源
int num = 0;
// 生产者线程工作函数
void *Produce(void *v)
{
while (1)
{
int time = rand() % 100 + 1;
usleep(time*10000);
pthread_mutex_lock(&mutex); // 抢锁
printf ("生产资源\n");
num += 2;
// 唤醒所有在条件变量 cond 上进行等待的线程
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex); // 解锁
}
}
// 消费者线程工作函数
void *Consum(void *v)
{
while (1)
{
int time = rand() % 100 + 1;
usleep(time*10000);
pthread_mutex_lock(&mutex); // 抢锁
/*
if (num == 0)
{
printf ("没有资源\n");
pthread_mutex_unlock(&mutex); // 解锁
continue;
}
*/
while (num == 0)
{
// 陷入沉睡, 等待某个人条件变量满足
// 第一个参数是等待哪个条件变量,第二个参数是相关的互斥锁
// 一旦 进行 条件等待,会陷入沉睡,让出CPU资源,阻塞在当前位置
// 工作步骤:
// 1、解锁互斥锁,让其他线程可以抢夺这个锁
// 2、当被唤醒的时候,和所有在互斥锁上阻塞的线程一起抢夺锁,抢到就继续执行,抢不到 继续抢
// 外层需要有一个循环,判断当前条件是否满足,因为就算该线程被唤醒,num 也不一定就是一个大于 0 的数
pthread_cond_wait(&cond, &mutex);
}
num--;
printf ("消费一个消息, 剩余资源: %d\n", num);
pthread_mutex_unlock(&mutex); // 解锁
}
}
int main()
{
srand ((unsigned int)time(NULL));
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_t produceId;
pthread_t consumId;
// 创建生产者线程
pthread_create(&produceId, NULL, Produce, NULL);
int i = 0;
for (i = 0; i < 3; i++)
{
// 创建消费者线程
pthread_create(&consumId, NULL, Consum, NULL);
pthread_detach(consumId);
}
pthread_join(produceId, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}