Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

993
Visualizações
¿Cómo puedo esperar una función asíncrona de la función síncrona en Swift 5.5?

Al cumplir con los protocolos o anular los métodos de la superclase, es posible que no pueda cambiar un método para que sea async , pero es posible que desee llamar a algún código async . Por ejemplo, mientras estoy reescribiendo un programa para escribirlo en términos de la nueva concurrencia estructurada de Swift, me gustaría llamar a un código de configuración async al comienzo de mi conjunto de pruebas anulando la class func setUp() definida en XCTestCase . Quiero que mi código de configuración se complete antes de que se ejecute cualquiera de las pruebas, por lo que usar Task.detached o async { ... } no es apropiado.

Inicialmente, escribí una solución como esta:

 final class MyTests: XCTestCase { override class func setUp() { super.setUp() unsafeWaitFor { try! await doSomeSetup() } } } func unsafeWaitFor(_ f: @escaping () async -> ()) { let sema = DispatchSemaphore(value: 0) async { await f() sema.signal() } sema.wait() }

Esto parece funcionar bastante bien. Sin embargo, en Concurrencia de Swift: Detrás de escena , el ingeniero de tiempo de ejecución Rokhini Prabhu afirma que

Las primitivas como los semáforos y las variables de condición no son seguras para usar con la concurrencia de Swift. Esto se debe a que ocultan la información de dependencia del tiempo de ejecución de Swift, pero introducen una dependencia en la ejecución de su código... Esto viola el contrato de tiempo de ejecución de avance de subprocesos.

También incluye un fragmento de código de un patrón de código tan inseguro

 func updateDatabase(_ asyncUpdateDatabase: @Sendable @escaping () async -> Void) { let semaphore = DispatchSemaphore(value: 0) async { await asyncUpdateDatabase() semaphore.signal() } semaphore.wait() }

que es notablemente el patrón exacto que se me ocurrió (me parece muy divertido que el código que se me ocurrió es exactamente el cambio de nombre de módulo de código incorrecto canónico).

Desafortunadamente, no he podido encontrar ninguna otra forma de esperar a que se complete el código asíncrono desde una función síncrona. Además, no he encontrado ninguna forma de obtener el valor de retorno de una función asíncrona en una función síncrona. Las únicas soluciones que he podido encontrar para esto en Internet parecen tan incorrectas como la mía, por ejemplo, este artículo de The Swift Dev dice que

Para llamar a un método asíncrono dentro de un método de sincronización, debe usar la nueva función de separación y aún debe esperar a que las funciones asíncronas se completen usando las API de envío.

que creo que es incorrecto o al menos inseguro.

¿Cuál es una forma correcta y segura de esperar a que una función async de una función síncrona funcione con los requisitos de protocolo o clase síncrona existentes, no específicos de las pruebas o XCTest? Alternativamente, ¿dónde puedo encontrar documentación que explique las interacciones entre async / await en Swift y las primitivas de sincronización existentes como DispatchSemaphore ? ¿Nunca son seguros o puedo usarlos en circunstancias especiales?

Actualizar:

Según la respuesta de @TallChuck, que notó que setUp() siempre se ejecuta en el hilo principal, descubrí que puedo bloquear intencionalmente mi programa llamando a cualquier función @MainActor . Esta es una excelente evidencia de que mi solución debe ser reemplazada lo antes posible.

Explícitamente, aquí hay una prueba que se cuelga.

 import XCTest @testable import Test final class TestTests: XCTestCase { func testExample() throws {} override class func setUp() { super.setUp() unsafeWaitFor { try! await doSomeSetup() } } } func doSomeSetup() async throws { print("Starting setup...") await doSomeSubWork() print("Finished setup!") } @MainActor func doSomeSubWork() { print("Doing work...") } func unsafeWaitFor(_ f: @escaping () async -> ()) { let sema = DispatchSemaphore(value: 0) async { await f() sema.signal() } sema.wait() }

Sin embargo, no se cuelga si @MainActor está comentado. Uno de mis temores es que si alguna vez llamo al código de la biblioteca (de Apple o de otro tipo), no hay forma de saber si eventualmente llamará a una función @MainActor , incluso si la función en sí no está marcada como @MainActor .

Mi segundo temor es que incluso si no hay @MainActor , todavía no sé si tengo la garantía de que esto es seguro. En mi computadora, esto cuelga.

 import XCTest @testable import Test final class TestTests: XCTestCase { func testExample() throws {} override class func setUp() { super.setUp() unsafeWaitFor { unsafeWaitFor { unsafeWaitFor { unsafeWaitFor { unsafeWaitFor { unsafeWaitFor { print("Hello") } } } } } } } } func unsafeWaitFor(_ f: @escaping () async -> ()) { let sema = DispatchSemaphore(value: 0) async { await f() sema.signal() } sema.wait() }

Si esto no funciona para usted, intente agregar más unsafeWaitFor s. Mi VM de desarrollo tiene 5 núcleos, y esto es 6 unsafeWaitFor s. 5 funciona bien para mí. Esto es claramente diferente a GCD. Aquí hay un equivalente en GCD que no se cuelga en mi máquina.

 final class TestTests: XCTestCase { func testExample() throws {} override class func setUp() { super.setUp() safeWaitFor { callback in safeWaitFor { callback in safeWaitFor { callback in safeWaitFor { callback in safeWaitFor { callback in safeWaitFor { callback in print("Hello") callback() } callback() } callback() } callback() } callback() } callback() } } } func safeWaitFor(_ f: @escaping (() -> ()) -> ()) { let sema = DispatchSemaphore(value: 0) DispatchQueue(label: UUID().uuidString).async { f({ sema.signal() }) } sema.wait() }

Esto está bien porque GCD está feliz de generar más subprocesos que CPU tiene. Entonces, tal vez el consejo es "solo use tantos unsafeWaitFor s como CPU tenga", pero si ese es el caso, me gustaría ver en algún lugar que Apple lo haya explicado explícitamente. En un programa más complejo, ¿puedo estar realmente seguro de que mi código tiene acceso a todos los núcleos de la máquina, o es posible que alguna otra parte de mi programa esté usando los otros núcleos y, por lo tanto, el trabajo solicitado por unsafeWaitFor nunca estar programado?

Por supuesto, el ejemplo en mi pregunta es sobre pruebas, y en ese caso, es fácil decir "realmente no importa cuál sea el consejo: si funciona, funciona, y si no funciona, el la prueba falla y usted la arreglará", pero mi pregunta no se trata solo de las pruebas; eso fue solo un ejemplo

Con GCD, me he sentido confiado en mi capacidad para sincronizar código asíncrono con semáforos (en mis propias DispatchQueue que controlo, y no en el subproceso principal) sin agotar el total de subprocesos disponibles. Me gustaría poder sincronizar el código async desde una función síncrona con async / await en Swift 5.5.

Si algo como esto no es posible, también aceptaría la documentación de Apple explicando exactamente en qué casos puedo usar con seguridad unsafeWaitFor o técnicas de sincronización similares.

over 4 years ago · Santiago Trujillo
4 Respostas
Responde à pergunta

0

Tal vez podría argumentar que el código asíncrono no pertenece a setUp() , pero me parece que hacerlo sería combinar sincronicidad con secuencial... ¿icity? El objetivo de setUp() es ejecutarse antes de que comience a ejecutarse cualquier otra cosa, pero eso no significa que deba escribirse sincrónicamente, solo que todo lo demás debe verse como una dependencia.

Afortunadamente, Swift 5.5 presenta una nueva forma de manejar las dependencias entre bloques de código. Se llama la palabra clave await (quizás hayas oído hablar de ella). Lo más confuso sobre async / await (en mi opinión) es el problema del huevo y la gallina de doble cara que crea, que en realidad no se aborda muy bien en ningún material que haya podido encontrar. Por un lado, solo puede ejecutar código asíncrono (es decir, usar await ) desde código que ya es asíncrono y, por otro lado, el código asíncrono parece definirse como cualquier cosa que use await (es decir, ejecuta otro código asíncrono).

En el nivel más bajo, eventualmente debe haber una función async que realmente haga algo asíncrono. Conceptualmente, probablemente se vea así (tenga en cuenta que, aunque está escrito en forma de código Swift, esto es estrictamente pseudocódigo):

 func read(from socket: NonBlockingSocket) async -> Data { while !socket.readable { yieldToScheduler() } return socket.read() }

En otras palabras, contrariamente a la definición del huevo y la gallina, esta función asincrónica no se define mediante el uso de una declaración de await . Repetirá hasta que los datos estén disponibles, pero se permite que se le anule mientras espera.

En el nivel más alto, necesitamos poder generar código asíncrono sin esperar a que finalice. Cada sistema comienza como un solo subproceso y debe pasar por algún tipo de proceso de arranque para generar los subprocesos de trabajo necesarios. En la mayoría de las aplicaciones, ya sea en una computadora de escritorio, un teléfono inteligente, un servidor web o lo que sea, el subproceso principal ingresa en una especie de bucle "infinito" donde, tal vez, maneja los eventos del usuario o escucha las conexiones de red entrantes, y luego interactúa con los trabajadores de manera adecuada. En algunas situaciones, sin embargo, un programa debe ejecutarse hasta su finalización, lo que significa que el subproceso principal debe supervisar la finalización exitosa de cada trabajador. Con los subprocesos tradicionales, como la biblioteca pthread de POSIX, el subproceso principal llama a pthread_join() para un determinado subproceso, que no regresará hasta que finalice ese subproceso. Con la concurrencia de Swift, no puedes hacer nada de eso (que yo sepa).

La propuesta de simultaneidad estructurada permite que el código de nivel superior llame a funciones async , ya sea mediante el uso directo de la palabra clave await o marcando una clase con @main y definiendo una función miembro static func main() async . En ambos casos, esto parece implicar que el tiempo de ejecución crea un subproceso "principal", activa su código de nivel superior como trabajador y luego llama a algún tipo de función join() para esperar a que finalice.

Como se demuestra en su fragmento de código, Swift proporciona algunas funciones de biblioteca estándar que permiten que el código síncrono cree Task . Las tareas son el componente básico del modelo de concurrencia de Swift. La presentación de WWDC que citó explica que el tiempo de ejecución está destinado a crear exactamente tantos subprocesos de trabajo como núcleos de CPU. Más tarde, sin embargo, muestran la imagen a continuación y explican que se requiere un cambio de contexto cada vez que se necesita ejecutar el hilo principal.

ingrese la descripción de la imagen aquí

Según tengo entendido, la asignación de subprocesos a los núcleos de la CPU solo se aplica al "Grupo de subprocesos cooperativos", lo que significa que si su CPU tiene 4 núcleos, en realidad habrá 5 subprocesos en total. El hilo principal está destinado a permanecer mayormente bloqueado, por lo que los únicos cambios de contexto serán las raras ocasiones en que el hilo principal se despierta.

Es importante entender que bajo este modelo basado en tareas, es el tiempo de ejecución, no el sistema operativo, el que controla los cambios de "continuación" (no lo mismo que los cambios de contexto). Los semáforos, por otro lado, operan a nivel del sistema operativo y no son visibles para el tiempo de ejecución. Si intenta usar semáforos para comunicarse entre dos tareas, puede hacer que el sistema operativo bloquee uno de sus subprocesos. Dado que el tiempo de ejecución no puede rastrear esto, no generará un nuevo hilo para ocupar su lugar, por lo que terminará infrautilizado en el mejor de los casos y estancado en el peor de los casos.

Bien, finalmente , en Meet async/await en Swift , se explica que la biblioteca XCTest puede ejecutar código asíncrono "listo para usar". Sin embargo, no está claro si esto se aplica a setUp() o solo a las funciones de casos de prueba individuales. Si resulta que admite una función asincrónica setUp() , entonces su pregunta de repente no tiene ningún interés. Por otro lado, si no lo admite, entonces está atascado en la posición de que no puede esperar directamente en su función async , pero tampoco es lo suficientemente bueno como para activar una Task no estructurada (es decir, una tarea que dispara y olvidar).

Su solución (que veo como una solución alternativa: la solución adecuada sería que XCTest admita una configuración async setUp() ), bloquea solo el hilo principal y, por lo tanto, debería ser seguro de usar.

over 4 years ago · Santiago Trujillo Relatório

0

Puedes llamar

_runAsyncMain { *async stuff here* }

Para ejecutar funciones asíncronas de nivel superior.

over 4 years ago · Santiago Trujillo Relatório

0

También me encontré con este problema para la configuración en XCTestCases. Específicamente Iniciar sesión para mi API.

La solución a la que llegué fue dividir Mi proceso de inicio de sesión en XCTestCases separados. Se ejecutan en orden alfabético. Así que configuré el siguiente caso de prueba con una prueba en el caso de prueba anterior.

algo como esto:

  1. NetworkLogin (nombre de usuario/contraseña de inicio de sesión)
  2. NetworkLoginRefresh (token de actualización)
  3. Pruebas de red (todas las llamadas API además de las cosas de autenticación)
  4. RedXCerrar sesión
over 4 years ago · Santiago Trujillo Relatório

0

Para esperar su función de configuración asíncrona, simplemente puede usar XCTestCase.expectation() con XCTestCase.wait() en su método de setUp :

 func doSettingUp() async { print("Start setting up... (wait 3 secs)") Thread.sleep(forTimeInterval: 3) print("Finish setting up") } class TestTests: XCTestCase { override func setUp() { print("setUp") let exp = expectation(description: "Test") Task { await doSettingUp() exp.fulfill() } wait(for: [exp], timeout: 10) } override func tearDown() { print("tearDown") } func test_Test() { print("test_Test") } }

Salidas:

 setUp Start setting up... (wait 3 secs) Finish setting up test_Test tearDown
over 4 years ago · Santiago Trujillo Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda