由于线程共享进程的资源和地址空间,因此对这些资源进行操作时,必须考虑到线程间资源访问的同步与互斥问题。
例如int *a int *b分别指向两块内存,上面的值分别初始化为(200, 100)
线程A执行这样的一个操作:将*a的值减少50,*b的值增加50.
线程B执行:打印出(a 跟 b 指向的内存的值的和)。
如果串行运行:A: *a -= 50; *b += 50;
如果并发执行,则有可能会出现一下调度:*a -= 50; printf("%d\n", *a + *b); *b += 50;
因此我们可以引入互斥锁,在对共享数据读写时进行锁操作,实现对内存的访问以互斥的形式进行。
(1)互斥锁线程控制
互斥锁通过简单的加锁方式来控制对共享资源的原子操作,它提供一个可以在同一时间,只让一个线程访问资源的的操作接口。
互斥锁有两种状态,也就是上锁和解锁。同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作,如果其他线程想要上锁已被上锁的互斥锁,该线程就会挂起,直到上锁的线程释放掉互斥锁为止。可以说,互斥锁保证了让每个线程对共享资源按顺序的原子操作。
POSIX互斥锁相关函数主要有以下5个:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
这些函数第一个参数mutex指向要操作的目标互斥锁,成功时返回0,出错返回错误码
pthread_mutex_init用于初始化互斥锁,mutexattr用于指定互斥锁的属性,若为NULL,则表示默认属性。除了用这个函数初始化互斥所外,还可以用如下方式初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_destroy用于销毁互斥锁,以释放占用的内核资源,销毁一个已经加锁的互斥锁将导致不可预期的后果
pthread_mutex_lock以原子操作给一个互斥锁加锁。如果目标互斥锁已经被加锁,则pthread_mutex_lock则被阻塞,直到该互斥锁占有者把它给解锁
pthread_mutex_trylock和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否加锁,是pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock进行加锁操作;否则将返回EBUSY错误码。注意:这里讨论的pthread_mutex_lock和pthread_mutex_trylock是针对普通锁而言的,对于其他类型的锁,这两个加锁函数会有不同的行为
pthread_mutex_unlock以原子操作方式给一个互斥锁进行解锁操作。如果此时有其他线程正在等待这个互斥锁,则这些线程中的一个将获得它
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
int a = 200;
int b = 100;
pthread_mutex_t lock;
void* ThreadA(void*) //线程A
{
pthread_mutex_lock(&lock); //上锁
a -= 50;
sleep(5); //执行到一半 使用sleep 放弃cpu调度
b += 50;
pthread_mutex_unlock(&lock); //解锁
}
void* ThreadB(void*) //线程B
{
sleep(1); //放弃CPU调度 目的先让A线程运行。
pthread_mutex_lock(&lock);
printf("%d\n", a + b);
pthread_mutex_unlock(&lock);
}
int main()
{
pthread_t tida, tidb;
pthread_mutex_init(&lock, NULL);
pthread_create(&tida, NULL, ThreadA, NULL);
pthread_create(&tidb, NULL, ThreadB, NULL);
pthread_join(tida, NULL);
pthread_join(tidb, NULL);
pthread_mutex_destroy(&lock);
return 0;
}
(2)信号量线程控制
主要用到的函数:
信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件 semaphore.h 中#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
这些函数的第一个参数sem指向被操作的信号量,上面这些函数成功时返回0,失败返回-1并设置errno。
sem_init函数用于初始化一个未命名的信号量(POSIX信号量API支持命名信号量,不过在该章节没有讨论)。pshared制定信号量的类型,如果其值为0,则表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享。value制定信号量的初始值,此外初始化一个已经被初始化的信号量将导致不可预期的后果
sem_destroy用于销毁信号量,以释放其占用的内核资源。如果销毁一个正在等待的信号量,则将导致不可预期的后果
sem_wait以原子操作将信号量值减1,如果信号量的值为0,则sem_wait将被阻塞,直到该信号量值为非0值
sem_trywait与sem_wait函数类似,不过它始终立即返回,而不论信号量是否具有非0值,相当于sem_wait的非阻塞版本。当信号量的值为非0时,sem_trywait对信号量执行减1操作;当信号量为0时,它将返回-1并设置errno为EAGAIN
sem_post以原子操作的方式将信号量的值加1,当信号量的值大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#define err_sys(msg) \
do { perror(msg); exit(-1); } while(0)
#define err_exit(msg) \
do { fprintf(stderr, msg); exit(-1); } while(0)
void *r1(void *arg)
{
sem_t* sems = (sem_t *)arg;
static int cnt = 10;
while(cnt--)
{
sem_wait(sems);
printf("I am in r1. I get the sems.\n");
}
}
void *r2(void *arg)
{
sem_t* sems = (sem_t *)arg;
static int cnt = 10;
while(cnt--)
{
printf("I am in r2. I send the sems\n");
sem_post(sems);
sleep(1);
}
}
int main(void)
{
sem_t sems;
pthread_t t1, t2;
printf("sems size: %d\n", sizeof(sems));
/* sem_init()第二个参数为0表示这个信号量是当前进程的局部信号量,否则该信号
* 就可以在多个进程之间共享 */
if(sem_init(&sems, 0, 0) < 0)
err_sys("sem_init error");
pthread_create(&t1, NULL, r1, &sems);
pthread_create(&t2, NULL, r2, &sems);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sems);
return 0;
}