线程控制2

3.私有数据(一键多值技术)

  多线程环境下,进程内的所有线程共享进程的数据空间,因此全局变量为所有线程公有。有些时候,需要保存线程自己的全局变量,这个全局变量仅在某个线程内有效,各个函数均可以访问该线程的私有全局变量,这个就是一键多值技术,即一个键对应多个数值。

  访问数据时都是通过键值来访问,看起来是在访问一个变量,实际在访问不同的数据

#include<pthread.h>
int pthread_key_create(pthread_key_t *key, void (*dest_function)(void *));//创建一个键
int pthread_setspecific(pthread_key_t key, const void *pointer);//为一个键设置线程私有数据
void *pthread_getspecific(pthread_key_t key);//从一个键读取线程私有数据
int pthread_key_delete(pthread_key_t key);//删除一个键

pthread_key_create:从Linux的TSD池中分配一项,将其赋值给key供后访问使用,第一个参数key为指向键值的指针,第二个参数为一个函数指针,如果指针不为空,则在线程退出时将以key所关联的数据为参数调用destr_function,释放分配的缓冲区。

key值一旦被创建,所有线程都可以访问他,各个线程可以根据自己的需要向key中填值,相当于提供了一个同名而不同值的全局变量,一键多值。

pthread_setspecific:该函数将pointer的值与key值相关联,用pthread_setspecific为一个键值指定新的线程函数时,线程必须先释放原有的线程数据以回收空间。

pthread_getspecific:通过该函数得到与key相关联的数据。

pthread_key_delete:该函数用来删除一个键,删除后,键所占用的内存将被释放。注意当键占用的内存被释放后,与该键关联的线程数据所占用的内存并不会被释放。因此,线程数据的释放必须在释放键之前完成。

例程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>

pthread_key_t key;


void func()
{
	printf("key's value is %d\n", pthread_getspecific(key));
}
void *thread2(void *arg)
{
	int tsd = 5;
	printf("thread %ld is running\n", pthread_self());
	pthread_setspecific(key, (void*)tsd);
	printf("thread2 %ld return %d\n", pthread_self(), pthread_getspecific(key));
	func();
}


void *thread1(void *arg)
{
	int tsd = 0;
	pthread_t thid2;
	printf("thread %ld is running\n", pthread_self());
	pthread_create(&thid2, NULL, thread2, NULL);
	sleep(2);
	printf("thread1 %ld returns %d\n",pthread_self(), pthread_getspecific(key));
	func();
}
int main()
{
	pthread_t thid;
	printf("main thread begins running\n");
	pthread_key_create(&key, NULL);
	pthread_create(&thid, NULL, thread1, NULL);
	sleep(3);
	pthread_key_delete(key);
	printf("main thread exit\n");
	return 0;
}

 运行结果

 

 可以看到,每个线程在调用func函数时返回的仍然是自己线程设置的key值。

4.线程同步

4.1互斥锁

多线程通过为关键代码加锁的方式来实现线程间的同步。

互斥锁函数

#include<pthread.h>
pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);//初始化一个互斥锁
pthread_mutex_destory(pthread_mutex_t *mutex);//注销一个互斥锁
pthread_mutex_unlock(pthread_mutex_t *mutex);//加锁,若不成功,阻塞等待
pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
pthread_mutex_trylock(pthread_mutex_t *mutex);//测试加锁,若不成功立即返回,错误码为EBUSY

 使用互斥锁之前必须先进行初始化操作。初始化的方式有两种,一种是静态赋值法,将宏常量PTHREAD_MUTEX_INITIALIZER赋值给互斥锁

pthread_mutex_t mutex = PTHREAD_MUTEX_INITILIZER;

 另外一种方式就是通过pthread_mutex_init函数初始化互斥锁,函数原型如上

参数mutexattr表示互斥锁的属性,如果为NULL则使用默认属性。

初始化结束后就可以使用pthread_mutex_lock,pthread_mutex_trylock这两个函数给互斥锁加锁了。

用pthread_mutex_lock()加锁时,如果mutex已经被锁住,当前尝试加锁的线程就会阻塞,直至互斥锁被其他线程释放。当pthread_mutex_lock函数返回时,说明互斥锁已经被当前线程加锁成功。pthread_mutex_lock则不同,若mutex已经被加锁,他将立即返回,返回的错误码为EBUSY,而不是阻塞等待。

注:加锁时,无论何种类型的锁,都不可能同时被两个不同的线程同时获得,其中一个必须等待解锁。在同一进程中的线程,若加锁后没有解锁,其他线程将无法再获得该锁。

锁用完后应当解锁,使用pthread_mutex_unlock函数解锁时,要满足两个条件:一个是互斥锁必须处于加锁状态,二是调用本函数的线程必须是给互斥锁加锁的线程(即加锁前锁还在,谁加锁谁解锁)。

互斥锁使用完毕后,必须进行清除。清除互斥锁使用函数pthread_mutex_destory,函数原型如上。

清除一个互斥锁将会释放它所占用的资源。清除互斥锁时要求锁处于放开的状态。若锁处于锁定状态,函数返回EBUSY,该函数成功执行时返回0。由于在Linux中,互斥锁并不占用内存,因此pthread_mutex_destory()除了解除互斥锁的状态外再无任何作用。

例程(文件读写保护)

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>

#define File  "output.txt"

pthread_mutex_t number_mutex;

void *pthread1(void *arg)
{
	int t = 5;
	while(t)
	{
		write_max(File);
		sleep(1);
	 	--t;
	}
}

void *pthread2(void *arg)
{
	int t = 5;
	while(t)
	{
		read_max(File);
		sleep(1);
		--t;
	}
}
void write_max(char *file)
{
	unsigned long long a = 0;
	int b = 0;
	char timeStr[14], ch;
	time_t timer;
	struct tm *tblock;
	time(&timer);
	tblock = gmtime(&timer);
	a = (tblock->tm_year+1900)*100;
	a = (a+tblock->tm_mon+1)*100;
	a = (a+tblock->tm_mday)*100;
	a = (a+tblock->tm_hour+8)*100;
	a = (a+tblock->tm_min)*100;
	a = (a+tblock->tm_sec);
	sprintf(timeStr, "%llu", a);
	pthread_mutex_lock(&number_mutex);
	FILE *fp;
	fp = fopen(file, "w");
	if(fp == NULL)
	{
		printf("Open file failed\n");
		fclose(fp);
		exit(1);
	}
	while(timeStr[b] != '\0')
	{
		fputc(timeStr[b], fp);
		++b;
	}
	printf("pthread = %ld writing %s to %s\n", pthread_self(), timeStr, file);
	fclose(fp);
	pthread_mutex_unlock(&number_mutex);
}
void read_max(char *file)
{
	char ch[14], *rc = NULL;
	pthread_mutex_lock(&number_mutex);
	FILE *fp;
	fp = fopen(file, "r");
	if(fp == NULL)
	{
		printf("Open file error\n");
		fclose(fp);
		exit(1);
	}
	rc = fgets(ch, 15, fp);
	printf("pthread = %ld, time = %s\n", pthread_self(), ch);
	fclose(fp);
	pthread_mutex_unlock(&number_mutex);
}


int main()
{
	pthread_t th1, th2;
	pthread_create(&th1, NULL, pthread1, NULL);
	pthread_create(&th2, NULL, pthread2, NULL);
	sleep(6);
	return 0;
}

 运行结果

 

 当程序中多个线程都要对一个文件进行读写操作的时候,为保证同步和文件安全,在一个线程对文件进行操作时必须加锁,其余要使用文件的线程先等待锁被释放后随机获得互斥锁。

(将写和读程序的加锁解锁代码都注释以后)

 

 同时对文件操作导致未知的数据出现在文件中。

4.2条件变量

条件变量是利用线程间共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段程序,否则只能等待条件成立。

有点复杂哈,可以这样先看一下条件变量的意义,就知道为啥要用它了

http://www.blogjava.net/fhtdy2004/archive/2009/07/05/285519.html

 

使用条件变量主要包括两个动作:一个等待使用资源的线程等待“条件变量被设置为真”;另一个线程在使用完资源后“设置条件为真”,这样就可以保证线程间的同步了。这其中存在一个关键问题,就是要保证条件变量可以被正确的修改,条件变量需要收到特殊保护,实际使用中互斥锁扮演着这样一个保护者的角色。

与互斥锁一样,条件变量的初始化也有两种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋予互斥锁,操作语句如下:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

 另一种方式是使用函数pthread_cond_init,它的原型如下

int pthread_cond_int(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

其中cond_attr参数是条件变量的属性,由于其并没有得到实现,所以它的值通常为NULL

等待条件成立有两个函数:pthread_cond_wait和pthread_cond_timewait。它们的原型如下

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

 pthread_cond_wait 函数释放由mutex指向的互斥锁,同时使当前线程关于cond指向的条件变量阻塞,直到条件被信号唤醒。通常条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞。当一个线程改变条件变量的值时,条件变量获得一个信号,使得等待条件变量的线程退出阻塞状态。

这里说一下自己的理解:

在thread_cond_wait()之前,必须先lock相关联的mutex,因为假如目标条件未满足,pthread_cond_wait()实际上会解锁(unlock)该互斥锁(mutex)然后阻塞(block),在目标条件满足后再重新lock该mutex, 然后返回

例程

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>


pthread_mutex_t mutex;
pthread_cond_t cond;
int i = 1;

void *thread1(void *arg)
{
	pthread_cleanup_push(pthread_mutex_unlock, &mutex);
	while(1)
	{
		pthread_mutex_lock(&mutex);
		printf("thread1 is locking\n");
		++i;
		pthread_cond_signal(&cond);
		printf("thread1 change the value of i, i = %d\n", i);
		pthread_mutex_unlock(&mutex);
		printf("thread1 is unlocked\n");
		sleep(4);
	}
	pthread_cleanup_pop(0);
}

void *thread2(void *arg)
{

	while(1)
	{
		pthread_mutex_lock(&mutex);
		printf("thread2 is locking\n");
		pthread_cond_wait(&cond, &mutex);
		printf("thread2 applied the condition, i = %d\n", i);
		pthread_mutex_unlock(&mutex);
		printf("thread2 is unlocked\n");
		sleep(1);
	}
}


int main()
{
	pthread_t tid1, tid2;
	printf("condition varable study!\n");
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(&cond, NULL);
	pthread_create(&tid1, NULL, (void*)thread1, NULL);
	pthread_create(&tid2, NULL, (void*)thread2, NULL);


	sleep(30);
	pthread_exit(0);
}

 运行结果看一下(片段结果)

 

可以看到线程使用等待条件变量后,看起来是加锁状态,实则已经解锁阻塞,等待条件变量被激活。

注:函数pthread_cleanup_push 和 pthread_cleanup_pop,他们提供回调函数保护。pthread_cond_wait() 和 pthread_cond_timewait() 都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex 后退出 pthread_cond_wait(),然后执行取消动作。也就是说,如果 pthread_cond_wait() 被取消,mutex 将依然保持锁定状态, 那么 thread1就需要定义退出回调函数来为其解锁。

pthread_cond_timewait 函数和 pthread_cond_wait 用法类似,区别在于pthread_cond_timewait 函数将阻塞直到条件变量获得信号或者经过有abstime指定的时间,也就是说,如果在给定的时刻前条件没有被满足,则返回ETIMEOUT,结束等待。

线程被条件变量阻塞后,可通过函数pthread_cond_signal 和 pthread_cond_broadcast 激活,其函数原型如下

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

 pthread_cond_signal激活一个等待条件成立的线程,存在多个线程时,按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

当一个条件变量不再使用时,需要将其清除。清除一个条件变量通过调pthread_cond_destory()实现,函数原型如下

int pthread_cond_destory(pthread_cond_t *cond)

 pthread_cond_destory 函数清除由cond指向的条件变量。注意,仅在线程没有等待该条件变量时才能清除这个条件变量,否则返回EBUSY。

4.3异步信号

  在Linux操作系统中,线程是在内核外实现的,进程时在内核中实现的,Linux线程本质上是轻量型的进程。我们知道,进程间相互独立,那么进程间通信如何进行呢?这时候信号就被引入,用来保证进程间通信。一个进程通过信号通知另一个进程发生了某个事件,例如该进程所需的数据已经就绪。线程同进程一样也可以接受和处理信号,信号也是线程间同步的手段之一。

  信号(如SIGINT 和 SIGIO)与任何进程都是异步的,也就是说信号到达线程的时间是不定的。如果有多个线程可以接收异步信号,则只有一个被选中。如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理。如果所有的线程都屏蔽该信号,则这些信号将被挂起,知道有线程解除屏蔽来处理它们。

Linux多线程拓展函数中有三个函数用于处理异步信号:

int pthread_kill (pthread_t threadid, int signo);
int pthread_sigmask(int how, const sigset_t, *newmask, sigset_t *oldmask);
int sigwait(const sigset_t *set, int *sig); 

 函数pthread_kill用来向特定的线程发送信号signo.

函数pthread_sigmask用来设置现成的信号屏蔽码,但是不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护。

函数sigwait用来阻塞线程,等待set中指定的信号之一到达,并将到达的信号存入*sig中。

 

 

 

 

posted @ 2019-09-29 17:42  C_hp  阅读(207)  评论(0)    收藏  举报