操作系统系列:关于线程同步

2023-12-28 11:34:05

关于线程安全
C 库是为单线程函数编写的,许多库函数使用全局变量来存储中间结果,这意味着它们不是线程安全的。如果两个或多个线程同时访问同一个库函数,该函数可能会产生错误答案或内存异常错误。大多数库函数的文档应指定系统调用是否是线程安全的。

1 Posix互斥

Posix 线程机制(pthreads)提供了一个简单的互斥功能,即pthread库定义的数据类型pthread_mutex_t。 定义一个全局的pthread_mutex_t 实例,并在使用宏 PTHREAD_MUTEX_INITIALIZER 声明时进行初始化。

有三个系统调用可以操作互斥量,每个系统调用都将指向 pthread_mutex_t 的指针作为参数。

int pthread_mutex_lock(pthread_mutex_t *mutex);

该函数检查互互斥量是否被锁定。 如果不是,它将锁定互斥体并返回,从而允许调用线程继续。 如果互斥量被锁定,则线程会阻塞,直到互斥量解锁为止。 此时互斥量再次设置为锁定状态,但函数返回,允许线程继续,需要在线程进入其临界区之前调用。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

与pthread_mutex_lock函数类似,但pthread_mutex_trylock会立即返回。 如果互斥量先前已解锁并且线程已成功锁定互斥量,则返回值为零。 如果互斥量被锁定,则该函数返回一个非零值。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock将解锁锁定的互斥量,要由线程在离开其临界区后调用。

下面是一些框架示例代码,演示了如何在 pthread 中使用互斥锁,目标是确保一次只有一个线程位于其临界区中。

/* pthreadmutex.c - a demo of pthread mutual exclusion */
#include <pthread.h>
#include <stdlib.h> /* for random() */
#include <stdio.h> /* for printf() */
#include <unistd.h> /* for sleep() */

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /* declare the mutex globally */

void CriticalSection(int num)
{
  printf("Thread %d is in its critical section\n", num);
  sleep(random() % 4);  /* sleep for zero to four seconds */
  printf("Thread %d is leaving its critical section\n", num);
}

void NonCriticalSection(int num)
{
  printf("Thread %d is in its noncritical section\n", num);
  sleep(random() % 4);   /* sleep for zero to four seconds */
  printf("Thread %d is leaving its noncritical section\n", num);
}

void* ThreadController(void *arg)
{
  int *num = (int *)malloc(sizeof(int));
  int i;
  *num = *(int *)arg;
  for (i=0;i < 4;i++) {
    NonCriticalSection(*num);
    pthread_mutex_lock(&mutex);
    CriticalSection(*num);
    pthread_mutex_unlock(&mutex);
  }
  pthread_exit(num);
  return num; /* we never get here */
}

int main()
{
  int i;
  int retval;
  int *arg;
  pthread_t threadid[5];

  for (i=0;i < 5;i++) {  /* create five threads */
    arg = (int *)malloc(sizeof(int));
    *arg = i+1;
    retval = pthread_create(&threadid[i],NULL, ThreadController,arg);
    if (retval != 0) {
      fprintf(stderr,"ERROR creating thread");
    }
    
  } 
  for (i=0;i<5;i++) {
    arg = (int *) malloc(sizeof(int));
    retval = pthread_join(threadid[i],(void **)&arg);
    if (retval == 0)
      printf("Thread %d finished with value %d\n",
	     i, *arg);
    else
      fprintf(stderr,"ERROR on join");
  }
  return 0;
}

该程序创建了五个线程,每个线程都会经过循环,该循环在 NonCriticalSection 和 CriticalSection 之间交替,关键部分由互斥锁保护。 如果开发者编译并运行该程序(不要忘记通过将 -lpthread 附加到编译语句来链接 pthread 库),应该能够确认一次只有一个线程位于其临界区中。

2 Unix 中的信号量

pthread 互斥量适用于线程,但由 fork 创建的多个独立进程的同步更为复杂。
信号量并不是原始 Unix 操作系统的一部分,它们是后来添加的,而且代码相当繁杂,这是进程间通信 (IPC) 设施的一部分,除了信号量之外,还包括共享内存消息传递机制

IPC进程间通信必须解决的问题是两个或多个独立进程必须共享一个公共结构,它不应该只供任何旧进程使用。 要获得对 IPC 设施的访问权限,进程必须知道密钥,键的类型为 key_t ,但它实际上只是一个整数。 一个进程创建并初始化信号量,其他进程如果知道密钥就可以访问它。使用 IPC 信号量的代码很复杂,而且对开发者不是很友好,我们不进一步讨论它,有兴趣可以通过阅读 semget、semctl 和 semop 的 Unix 手册页来进一步了解。

3 Win32 同步系统调用

3.1 Win32中的线程API系统调用

创建新线程的 Win32 API 是:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security attributes
  DWORD dwStackSize,                         // initial thread stack size
  LPTHREAD_START_ROUTINE lpStartAddress,     // pointer to thread function
  LPVOID lpParameter,                        // argument for new thread
  DWORD dwCreationFlags,                     // creation flags
  LPDWORD lpThreadId                         // pointer to receive thread ID
);

该函数的工作方式与pthread_create非常相似。如果第一个参数 lpThreadAttributes 设置为 NULL,第二个参数 dwStackSize 设置为零,则将分配适当的默认值。 第三个参数 lpStartAddress 应设置为要调用的函数的名称。 第四个参数 lpParameter 是指向要传递给函数的参数的指针。 第五个参数 dwCreationFlags 应设置为零。 最后一个参数是指向 DWORD 的指针,将由函数设置。
如果成功,该函数将返回一个 HANDLE 值。 如果失败,返回值将为NULL。

被调用的函数必须具有以下形式:

DWORD WINAPI ThreadProc(LPVOID lpParameter); /*将 ThreadProc 替换为函数名称*/

pthread_join 的 Win32 等效函数是:

DWORD WaitForSingleObject(
  HANDLE hHandle,        // handle to object to wait for
  DWORD dwMilliseconds   // time-out interval in milliseconds
);

这是一个非常重要的函数,因为它用于等待各种不同的事件,开发者经常用到它,该函数用于等待线程终止。

  • 第一个参数 hHandle 是从 CreateThread 返回的句柄。 与 pthread_join 一样,该函数会阻塞,直到线程终止;与 pthread_join 不同的是,此函数允许您指定在解除阻塞之前愿意等待事件的时间。
  • 第二个参数是等待的毫秒数。 如果该值为零,则即使线程尚未终止,该函数也会立即返回。 该值另一个可能的关键字是 INFINITE,如果线程不终止,它会导致函数无限期地阻塞。

退出线程而不退出进程的函数调用是:

VOID ExitThread(DWORD ExitCode);

ExitCode是返回给另一个线程的值。其它线程可以通过下面这个函数来读取退出API调用的线程返回的ExitCode,它通常在线程中止后调用,因此经常合并WaitForSingleObject一起使用:

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpdwExitCode);
WaitForSingleObject

请看下面这段示例代码:

#include <windows.h>
#include <stdio.h>

DWORD WINAPI ThreadRoutine(LPVOID lpArg)
{
    int a;
	a = *(int *)lpArg;
	fprintf(stderr,"My argument is %d\n",a);
	return NULL;
}

int main()
{
	int i;
	int *lpArgPtr;
	HANDLE hHandles[5];
	DWORD ThreadId;
	
	for (i=0;i < 5;i++) {
		lpArgPtr = (int *)malloc(sizeof(int));
		*lpArgPtr = i;
		hHandles[i] = CreateThread(NULL,0,ThreadRoutine,lpArgPtr,0,&ThreadId);
		if (hHandles[i] == NULL) {
			fprintf(stderr,"Could not create Thread\n");
			exit(0);
		}
		else printf("Thread %d was created\n",ThreadId);
	}

	for (i=0;i < 5;i++) {
		WaitForSingleObject(hHandles[i],INFINITE);
	}
	return 0;
}

3.2 Win32中的同步调用

Unix 中进程同步是后来添加的,但是Windows不同,线程和进程的同步是 Windows 操作系统的固有功能,并且 Win32 提供了非常丰富的同步 API 集。

如果开发者只想对进程或线程之间共享的变量进行递增、递减或设置值,WIN32 提供了以下函数。

LONG InterlockedIncrement(LPLONG lpAddent);

LONG InterlockedDecrement(LPLONG lpAddend);

LONG InterlockedExchange(LPLONG target, LONG Value);

前两个函数将指向整数的指针作为参数,并分别递增和递减该值,最后一个函数将其第一个参数的值设置为其第二个参数的值。 操作系统保证这些操作是原子操作,因此当两个或多个进程或线程尝试同时更新变量时,不会发生由于竞争条件而导致值不正确的问题。

WIN32 允许独立进程创建和使用互斥量,互斥量有一个句柄和一个名称(一个字符串),要创建新的互斥量,请使用此函数

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwer,
    LPCTSTR lpname
);

第一个参数可以设置为 NULL,第二个参数应设置为 FALSE,第三个参数是互斥锁的名称,该函数返回互斥锁的句柄。

两个或多个进程可以调用 CreateMutex 来创建相同的命名互斥体。 第一个进程实际上创建互斥体,后续进程打开现有互斥体的句柄。

相当于互斥锁功能的是 API:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

第一个参数是调用 CreateMutex 返回的句柄,第二个参数是放弃之前等待的时间。 如果第二个参数为零,则相当于 pthread 函数 pthread_mutex_trylock,即它立即返回。 返回值将是 WAIT_OBJECT_0(表示互斥锁已成功锁定)或 WAIT_TIMEOUT(表示互斥锁可用之前发生超时);如果第二个参数设置为INFINITE,则该函数相当于pthread函数pthread_mutex_lock。

相当于 pthread_mutex_unlock 的函数是:

BOOL ReleaseMutex(HANDLE hMutex);

4 练习

下面这个练习中,开发者将编写一个程序来实现线程并同步,程序的主线程应生成四个线程,线程号为 1 到 4。

四个线程中的每一个都应该进入一个在非关键部分和关键部分之间交替的循环。 4 个线程中的每一个都应该读取一个名为 datan.txt 的文件,其中 n 是线程号。 这些文件将由空格分隔的整数列表组成。 这些将告诉线程在非关键部分和关键部分中有多少秒。 例如,如果文件如下所示
7 3 5 1 2 4
该线程将在其非关键部分中花费 7 秒,然后在其关键部分中花费 3 秒,然后在其非关键部分中花费 5 秒,然后在其关键部分中花费 1 秒,然后在其非关键部分中花费 2 秒,然后在其关键部分中花费 4 秒 ,那么它应该返回。 返回值应该是在其关键部分花费的总时间(即偶数值的总和)。

当然,一次只能有一个线程位于其临界区中。 每个线程应该显示以下语句:

线程 n 正在进入其临界区

当它进入临界区时,并且

线程 n 正在离开其临界区

当它离开临界区时。

开发者可以使用 Sleep() API 控制每个函数所花费的时间,需要一个整数作为参数并休眠那么多毫秒(因此您应该将文件中的值乘以 1000)。

开发者可以使用以下四个文件来测试您的程序。

Data0.txt
4 5 6 7 10 3

Data1.txt
1 1 2 3 4 5 6 7

Data2.txt
2 4 6 8 10 12 10 8

Data3.txt
6 3 8 4 7 5 12 2

文章来源:https://blog.csdn.net/wss_xt/article/details/135260520
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。